Java没有提供任何机制来安全的终止线程. 但它提供了中断(Interruption)的协作机制, 使一个线程能终止另一个线程的当前工作.
一个行为良好的软件与勉强运行的软件之间的最主要区别就是, 行为良好的软件能很完善的处理失败, 关闭和取消等过程

每个线程中都有一个boolean类型的中断状态, 当中断线程时, 这个线程的中断状态将被置为true

  • interrupt方法能中断目标线程(发出中断请求, 线程在下一个合适的时刻中断自己)
  • isInterrupted方法返回目标线程的中断状态
  • interrupted方法将清除线程的中断状态, 并返回它之前的值, 这也是清除中断状态的唯一方法

image.png

任务取消

中断是实现取消的最合理方式
第一个程序, 如果生产者速度超过消费者, 那么程序会阻塞在queue.put上, 调用cancel方法仅设置了cancelled=true, 但无法走到下一个while循环, 生产者无法从阻塞状态恢复过来, 且消费者已经停止从队列中取出素数, 所以put方法会一直阻塞下去
image.png
使用中断优化后:

  • 当程序阻塞在queue.put方法上时, 生产者不会继续往队列塞元素
  • 调用cancel方法后程序也会立即检测到中断状态

image.png

中断策略

  • 最合理的中断策略是某种形式的线程级取消操作或服务级取消操作: 尽快退出, 在必要时进行清理, 通知某个所有者, 该线程已经退出, 这就是为啥大多数可阻塞的库函数只是抛出InterruptedException作为中断响应
  • 任务不会在其自己拥有的线程中执行(主线程), 而是在某个服务(如线程池)拥有的线程中执行, 对于非线程所有者的代码来说, 应该小心的保存中断状态, 这样拥有线程的代码才能对中断做出响应

响应中断

  • 传递异常, 从而使你的方法也成为可中断的阻塞方法
  • 恢复中断状态, 调用 Thread.currentThread().interrupt(), 从而使调用栈的上层代码能够对其进行处理

不可取消的任务调用可中断阻塞方法

  • 不能在捕获InterruptedException后立即恢复中断状态, 否则queue.take()会检查到已中断又再次抛出异常, 引发死循环(大多数可中断的阻塞方法都会在入口处检查中断状态, 并且当发现该状态已被设置时就会立即抛出InterruptedException)
  • 应该在循环中调用这些方法, 并在发现中断后重新尝试, 应该本地保存中断状态, 并在返回前恢复状态.
  • 如果代码不会调用可中断的阻塞方法, 那么依然可以通过在任务代码中轮询当前线程的中断状态来响应中断

image.png

计时运行以及处理任务执行过程中的异常

如下代码将在指定时间内运行一个任意的Runnable.

  • 破坏了规则: 在中断线程之前, 应该了解它的中断策略, 由于timedRun可以被任意一个线程中调用, 因此无法知道调用线程(taskThread)的中断策略, 甚至都不知道taskThread会不会响应中断
  • 且这种写法, 如果任务在超时之前完成, 那任务还会继续运行直至取消, 没有立即取消, 需要用schedule返回的scheduleFuture来优化, 增加了复杂度

image.png
优化后的计数器如下:

  • taskThread是一个专门运行任务的线程, 它的中断策略是已知的
  • 通过volatile的共享Throwable变量能够得知任务在执行过程中的异常, 并在调用者线程中得到处理
  • 使用限时的join方法, 即使任务不响应中断, 限时运行的方法仍能返回到它的调用者
  • 缺陷是join的不足, 无法知道执行控制是因为线程正常退出而返回还是因为join超时而返回

image.png
使用Future优化后的计时器如下 , 个人觉得, 该程序清单非常完美, 简直就是编程范例

  • Future.cancel(boolean mayInterruptIfRunning), 取消任务, 参数为true, 表示会尝试中断正在运行中的任务, 为false表示若任务还未启动, 就不要运行它
  • 当Future.get抛出InterruptedException或TimeoutException时, 如果你知道不再需要结果, 那么就可以调用Future.cancel来取消任务

image.png

处理不可中断的阻塞

如果一个线程由于执行同步的Socket I/O或者等待获得内置锁而阻塞, 那么中断请求只能设置线程的中断状态, 除此之外没有任何其他作用, 但可以使用类似于中断的手段来停止这些线程
如下示例, 改写interrupt方法关闭了socket连接
image.png
使用newTaskFor优化

  • 定义了一个CancellableTask接口, 拓展了Callable, 并增加了一个cancel方法以及newTask工厂方法来构造RunnableFuture
  • CancellingExecutor拓展了ThreadPoolExecutor, 并通过改写newTaskFor使得CancellableTask可以创建自己的Future

image.png

停止基于线程的服务

  • 线程有一个相应的所有者, 即创建该线程的类, 因此线程池是其工作者线程的所有者, 如果要中断这些线程, 那么应该使用线程池
  • 正确的封装原则是: 除非拥有某个线程, 否则不能对该线程进行操控, 如中断线程或者修改线程的优先级等
  • 对于持有线程的服务, 只要服务的存在时间大于创建线程的方法的存在时间, 那么就应该提供生命周期方法
  • 服务应该提供生命周期方法来关闭它自己, 以及它所拥有的线程

关闭ExecutorService

  • shutdown正常关闭
  • shutdownNow强行关闭, 首先关闭正在执行的任务, 然后返回所有尚未启动的任务清单

在复杂程序中, 通常会将ExecutorService封装在更高级别的服务中, 并且该服务能提供其自己的生命周期方法
image.png

毒丸对象

  • 另一种关闭生产者-消费者服务的方式是使用毒丸对象, 毒丸是指一个放在队列上的对象, 其含义是: 当得到这个对象时, 立即停止
  • 在FIFO(先进先出)队列中, 毒丸对象将确保消费者在关闭之前首先完成队列中的所有工作, 在提交毒丸对象之前提交的所有工作都被处理, 生产者在提交了毒丸后, 将不会再提交任何工作

记录线程池未执行完的任务

  • 使用shutdownNow, 会尝试取消正在执行的任务, 并返回所有已提交但尚未开始的任务, 将这些任务写入日志或者保存起来, 以便后续接着处理
  • 在TrackingExecutor中存在一个不可避免的竞态条件, 从而产生误报问题, 一些被认为已取消的任务实际上已经执行完成, 这是因为在任务执行最后一条指令以及线程池将任务记录为结束的两个时刻之间, 线程池可能被关闭, 如果任务是幂等的, 那么这不会存在问题

image.png
image.png