07_01_任务取消
任务取消
任务、线程、服务、应用程序之间的关系:
- 任务是最小的执行单元
- 任务在线程中执行
- 线程在服务(比如线程池服务)中运行
- 服务在应用程序(JVM)中运行
取消某个操作的请求有很多,比如:
- 用户请求取消
- 有时间限制的操作
- 应用程序事件
- 运行错误
- 应用程序关闭
不同层次的取消操作,可能会导致不同的结果。
Java 中没有一种安全的抢占式方法来停止线程,所以也没有安全的方式可以直接取消任务。
只有一些协作式的机制,取消任务只能通过这种方式来安全实现。
一、取消策略
一个可取消的任务必须有取消策略(Cancellation Policy):
- How:其他代码如何(How)请求取消任务
- When:任务在何时(When)检查是否已经请求了取消
- What:响应取消请求时,应该执行哪些(What)操作
取消策略详细定义了任务取消的整个流程。
代码示例:
1 | public class CancellationTask implements Runnable { |
取消策略的各个部分分别是:
- How:调用
cancel()
方法取消任务 - When:在循环前验证取消标志
while (!cancelled)
- What:没有其他操作,直接结束任务
对于每一种取消策略,都必须有它的 How
,When
,What
。
二、取消标志策略
最简单的取消策略,就是使用一个取消标志,而任务定期检查这个标志。
如果设置了取消标志,那么任务就将提前结束。
1 | public class MarkCancellationTask implements Runnable { |
通过在任务中检查取消标志,就可以实现取消任务的简单方式。
但是这种方式存在一个问题,那就是如果任务中执行了阻塞操作,任务就可能取消不了:
1 | public class MarkBlockCancellationTask implements Runnable { |
比如这里执行了阻塞操作 queue.put()
,假设操作一直阻塞,那么检查取消标志 canceled
的动作就一直不会触发。
这种情况下,任务没办法取消,只能一直等到阻塞结束后,才能够取消。
这其实和我们想要的效果不一样,我们想要的是那种取消后,任务就准备结束了。
三、线程中断策略
为了解决阻塞导致任务无法取消的问题,需要用到线程的中断:
- 每个线程都有一个中断状态标志
- 大部分阻塞操作都支持抛出线程中断异常
InterruptedException
- JVM 不能保证阻塞方法检测到中断的速度,但是总是可以检测到中断的
- 线程中断并不会直接中断任务运行,而只是传递了请求中断的消息
代码示例:
1 | public class ThreadInterruptCancellationTask implements Runnable { |
和取消标志差不多,只是线程中断提供的取消功能更全面:它可以取消阻塞的任务。
- 通常,中断是实现任务取消的最合理方式
所以,如果任务代码能够响应中断,最好使用中断作为取消策略。
四、处理不可中断的操作
中断虽然能满足大部分的需求,但是:
- 不是所有方法或阻塞操作都支持响应中断
比如 Socket I/O 等方法,都不支持中断。
但是,可以使用类似中断的手段来取消任务,比如 Socket 可以用 close()
方法结束阻塞:
1 | public class SocketReaderThread extends Thread { |
不过这种模拟中断的方式,需要事先了解线程阻塞的原因以及如何结束线程阻塞才行。
类似这种不可中断的阻塞,在 Java 类库中大约有这几种:
- Java.io 包中的 Socket I/O
- Java.io 包中的同步 I/O
- Selector 包中的异步 I/O
- 获取某个锁,指的是 Lock
针对这些情况,可以利用类似上面的方式来封装中断处理。