Java没有提供任何机制来安全地终止线程。但它提供了中断,这是一种协作机制,能够使一个线程终止另一个线程的当前工作。这种协作方法是必要的,我们很少希望某个任务、线程或服务立即停止,因为这种立即停止会使共享数据结构处于不一致的状态。在编写任务和服务时可以使用一种协作的方式:当需要停止时,它们首先会清楚当前旨在执行的工作,然后再结束。任务本身比发出取消请求的代码更清楚如何执行清除工作。行为良好的软件能很完善的处理失败、关闭和取消等过程。
7.1 任务取消
在Java中没有一种安全的抢占式方法来停止线程。只有一些协作式的救治,使请求取消的任务和代码都遵循一种协商好的协议。
例如设置某个取消标志,任务定期查看这个标志。为了使这个过程能可靠工作,需要将其设置为volatile类型。
7.1.1 中断
不可靠的取消操作将把生产者置于阻塞的操作中,例如任务:
while (!canelled) {
queue.put(...);
}
此时生产者速度大于消费者速度,生产者的put方法将被阻塞,消费者希望取消任务,那么会设置cancelled,这时消费者停止消费,生产者保持put阻塞,无法再去检测cancelled标志。
一些特殊的阻塞库的方法支持中断。线程中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉他在合适的或者可能的情况下停止当前工作,并转而执行其他的工作。
每个线程都有一个boolean类型的中断状态,interrupt能中断目标线程,isInterrupted能返回目标线程的中断状态,interrupted能清除当前线程的中断状态并返回它之前的值。
阻塞库方法,例如Thread.sleep、Object.wait等,都会检查线程合适中断,并在发现中断时提前返回。它们在响应中断时执行的操作包括:清除中断状态,抛出InterruptedException,表示阻塞操作由于中断而提前结束。JVM不能保证阻塞方法检测到中断的速度,但实际上还是非常快的。
对中断操作的正确理解:并不会真正的中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己(取消点)。
通常,中断是实现取消的最合理方式。
如果可中断的阻塞方法的调用频率并不高,不足以获得足够的响应性,那么显示的检测中断状态能起到一定的帮助作用。
7.1.2 中断策略
最合理的中断策略:以某种形式的线程级取消操作或服务级取消操作:尽快退出,在必要时进行清理,通知某个所有者该线程已经退出。
任务不应该对执行该任务的线程的中断策略做出任何假设,除非该任务被专门设计为在服务中运行,并且在这些服务中包含特定的中断策略。
如果除了将InterruptedException传递给调用者外还需要执行其他操作,那么应该在捕获InterruptedException之后恢复中断状态:Thread.currentThread().interrupt();
每个线程拥有各自的中断策略,除非你知道中断对该线程的含义,否则就不应该中断这个线程。线程应该只能由其所有者中断,所有者可以将线程的中断策略信息封装到某个合适的取消机制中,例如shutdown方法。
7.1.3 响应中断
两种实用策略可用于处理InterruptedException:
- 传递异常 (方法throws出去)从而使方法也成为可中断的阻塞方法
- 恢复中断状态 (再次调用interrupt来恢复中断)从而使调用栈中的上层代码能够对其进行处理
只有实现了线程中断策略的代码才可以屏蔽中断请求。
如果代码中不会调用可中断的阻塞方法,可以通过任务代码中轮询当前线程的中断状态来响应中断,选择合适的轮询频率是关键。
7.1.4 示例:计时运行
没看懂
7.1.5 通过Future来实现取消
如果任务在被取消前就抛出一个异常,那么该异常将被重新抛出以便由调用者来处理异常。
当Future.get抛出InterruptedException或TimeoutException时,如果你知道不再需要结果,那么就可以调用Future.cancel来取消任务。
7.1.6 处理不可中断的阻塞
再看一遍
7.1.7 采用newTaskFor来封装非标准的取消
写一遍
7.2 停止基于线程的服务
对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么就应该提供生命周期方法。
7.2.1 示例:日志服务
没看懂
7.2.2 关闭ExecutorService
shutdown:正常关闭
shutdownNow:强行关闭
7.2.3 “毒丸”对象
当消费者获取到毒丸对象时,立即停止。生产者提交毒丸对象后,不会再提交别的工作。适用于FIFO队列。
可以扩展到多个生产者消费者。但是只有在无界队列中,毒丸对象才能可靠的工作。
7.2.4 示例:只执行一次的服务
7.2.5 shutdownNow的局限性
强行关闭时,会尝试取消正在执行的任务,并返回所有已提交但尚未开始的任务,写入日志或保存起来。
我们无法通过常规方法找出哪些任务已经开始尚未结束,除非任务本身会执行某种检查。
幂等:Idempotent,任务执行两次与执行一次会得到相同的结果。
7.3 处理非正常的线程终止
导致线程提前死亡最主要的原因就是RuntimeException。由于这些异常表示出现了某种编程错误,或者其他不可修复的错误,因此通常不会被捕获。就不会再调用栈中逐层传递,而是默认地在控制太重输出栈追踪信息,并终止线程。
线程非正常退出的后果不确定,取决于线程在应用中的作用。
当编写一个向线程池提交任务的工作者线程类时,或者条用不可信的外部代码时,应该在try-catch中调用这些任务,捕获未检查的异常,或者可以使用try-finally代码块确保框架能够知道线程非正常退出的情况,并作出正确的响应。
未捕获异常的处理UncaughtExceptionHandler
Thread API中提供了UncaughtExceptionHandler,处理未捕获异常,能检测出异常终结的情况,能有效防止线程泄漏问题。
默认的UncaughtExceptionHandler是将栈追踪信息输出到System.err。
可以自定义,比如记日志,重启线程,关闭应用程序等。
execute提交的任务,可以通过UncaughtExceptionHandler来处理;
submit提交的任务,抛出的异常会被认为是任务返回状态的一部分,会被Future.get封装在ExecutionException中重新抛出。
7.4 JVM关闭
关闭:当最后一个正常(非守护)线程结束时;或调用了System.exit时;或通过其他平台特定的关闭方法时(Ctrl+C);或Runtime.halt,或在OS中杀死JVM进程(SIGKILL)。
7.4.1 关闭钩子
没看懂
7.4.2 守护线程
如果希望创建一个线程来执行一些辅助工作,但又不希望这线程阻碍JVM的关闭,这种情况需要使用守护线程(Daemon Thread)。
JVM启动是时创建的除了主线程,其他都是守护线程(例如GC)。当创建一个线程时,新线程继承创建它的线程的守护状态。因此默认,主线程创建的都是普通线程。
-
普通线程(非守护线程)
-
守护线程
当一个线程退出时,JVM会检查其他正在运行的线程,如果这些线程都是守护线程,JVM会正常退出操作。当JVM停止时,所有的守护线程将被抛弃——既不会执行finally代码块,也不会执行回卷栈,直接退出。
尽量少使用守护线程,特别是包含IO,很难在不进行清理下安全的抛弃。最好用于执行内部任务,例如周期性从内存的缓存中移除逾期的数据。
7.4.3 终结器
finalize