07_01_任务取消

任务取消

任务、线程、服务、应用程序之间的关系:

  • 任务是最小的执行单元
  • 任务在线程中执行
  • 线程在服务(比如线程池服务)中运行
  • 服务在应用程序(JVM)中运行

取消某个操作的请求有很多,比如:

  • 用户请求取消
  • 有时间限制的操作
  • 应用程序事件
  • 运行错误
  • 应用程序关闭

不同层次的取消操作,可能会导致不同的结果。

Java 中没有一种安全的抢占式方法来停止线程,所以也没有安全的方式可以直接取消任务。

只有一些协作式的机制,取消任务只能通过这种方式来安全实现。

一、取消策略

一个可取消的任务必须有取消策略(Cancellation Policy):

  • How:其他代码如何(How)请求取消任务
  • When:任务在何时(When)检查是否已经请求了取消
  • What:响应取消请求时,应该执行哪些(What)操作

取消策略详细定义了任务取消的整个流程。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CancellationTask implements Runnable {

private volatile boolean cancelled;

@Override
public void run() {
while (!cancelled) {
// do something
}
}

public void cancel() {
cancelled = true;
}
}

取消策略的各个部分分别是:

  • How:调用 cancel() 方法取消任务
  • When:在循环前验证取消标志 while (!cancelled)
  • What:没有其他操作,直接结束任务

对于每一种取消策略,都必须有它的 HowWhenWhat

二、取消标志策略

最简单的取消策略,就是使用一个取消标志,而任务定期检查这个标志。

如果设置了取消标志,那么任务就将提前结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MarkCancellationTask implements Runnable {

/**
* 取消标志
*/
private volatile boolean cancelled;

private final List<BigInteger> primes = new ArrayList<>();

@Override
public void run() {
BigInteger p = BigInteger.ONE;
while (!cancelled) {
p = p.nextProbablePrime();
synchronized (this) {
primes.add(p);
}
}
}

public void cancel() {
cancelled = true;
}
}

通过在任务中检查取消标志,就可以实现取消任务的简单方式。

但是这种方式存在一个问题,那就是如果任务中执行了阻塞操作,任务就可能取消不了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class MarkBlockCancellationTask implements Runnable {

/**
* 取消标志
*/
private volatile boolean cancelled;

private final BlockingDeque<BigInteger> queue;

public MarkBlockCancellationTask(BlockingDeque<BigInteger> queue) {
this.queue = queue;
}

@Override
public void run() {
try {
BigInteger p = BigInteger.ONE;
while (!cancelled) {
p = p.nextProbablePrime();
// 阻塞操作,可能会一致阻塞
queue.put(p);
}
} catch (InterruptedException e) {
//
}
}

public void cancel() {
cancelled = true;
}
}

比如这里执行了阻塞操作 queue.put(),假设操作一直阻塞,那么检查取消标志 canceled 的动作就一直不会触发。

这种情况下,任务没办法取消,只能一直等到阻塞结束后,才能够取消。

这其实和我们想要的效果不一样,我们想要的是那种取消后,任务就准备结束了。

三、线程中断策略

为了解决阻塞导致任务无法取消的问题,需要用到线程的中断:

  • 每个线程都有一个中断状态标志
  • 大部分阻塞操作都支持抛出线程中断异常 InterruptedException
  • JVM 不能保证阻塞方法检测到中断的速度,但是总是可以检测到中断的
  • 线程中断并不会直接中断任务运行,而只是传递了请求中断的消息

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ThreadInterruptCancellationTask implements Runnable {

private final BlockingDeque<BigInteger> queue;

public ThreadInterruptCancellationTask(BlockingDeque<BigInteger> queue) {
this.queue = queue;
}

@Override
public void run() {
try {
BigInteger p = BigInteger.ONE;
// 1. 和取消标志一样,定期检查中断标志
while (!Thread.currentThread().isInterrupted()) {
p = p.nextProbablePrime();
// 阻塞操作,可能会一致阻塞
queue.put(p);
}
} catch (InterruptedException e) {
// 2. 还提供了中断异常的取消方式
Thread.currentThread().interrupt();
}
}
}

和取消标志差不多,只是线程中断提供的取消功能更全面:它可以取消阻塞的任务。

  • 通常,中断是实现任务取消的最合理方式

所以,如果任务代码能够响应中断,最好使用中断作为取消策略。

四、处理不可中断的操作

中断虽然能满足大部分的需求,但是:

  • 不是所有方法或阻塞操作都支持响应中断

比如 Socket I/O 等方法,都不支持中断。

但是,可以使用类似中断的手段来取消任务,比如 Socket 可以用 close() 方法结束阻塞:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SocketReaderThread extends Thread {

@Override
public void interrupt() {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
super.interrupt();
}
}
}

不过这种模拟中断的方式,需要事先了解线程阻塞的原因以及如何结束线程阻塞才行。

类似这种不可中断的阻塞,在 Java 类库中大约有这几种:

  • Java.io 包中的 Socket I/O
  • Java.io 包中的同步 I/O
  • Selector 包中的异步 I/O
  • 获取某个锁,指的是 Lock

针对这些情况,可以利用类似上面的方式来封装中断处理。

作者

jiaduo

发布于

2022-05-15

更新于

2023-04-03

许可协议