取消与关闭
Java中没有提供任何机制来安全得终止线程,但它提供了中断(Interruption),这是一种协作机制,能够使一个线程终止另一个线程当前的工作。
我们很少希望某个任务、线程或服务立即停止,因为这种立即停止会使共享的数据结构处于不一致的状态。相反,在编写任务和服务时可以使用一种协作的方式:当需要停止时,它们首先清楚当前正在执行的工作,然后再结束。
1.任务取消
一个可取消的任务必须拥有取消策略(Cancellation Policy),在这个策略中应详细定义取消操作的“How”、“When”以及“What”,即其他代码如何(How)请求取消任务,任务在何时(When)检查是否已经请求了取消,以及在响应取消请求时应执行哪些(What)操作。
取消任务的方式:
- 设置一个取消标志,任务在每次迭代执行前首先检查此标志以确定任务是否被取消
- 中断。在任务中如果存在阻塞调用,那么第一种方法将失效,此时,可以通过中断方法取消任务
2.中断
Thread中的中断方法:
public class Thread{
public void interrupt(){...}
public boolean isInterrupted(){...}
/*静态的interrupted方法将清除当前线程的中断状态,并返回它之前的值,这也是清除中断状态的唯一方法*/
public static boolean interrupted(){...}
}
调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。
对中断操作的正确理解是:它并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。
并非所有阻塞调用都能响应中断操作,wait、sleep、join等将严格处理中断请求,而IO阻塞并不能响应(常规的)中断请求。
3.中断策略
中断策略规定线程如何解释某个中断请求----当发现中断请求时,应该做哪些工作。
最合理的中断策略是某种形式的线程级(Thread Level)取消操作或服务级(Service Level)取消操作:尽快推出,在必要时进行清理,同志某个进程所有者该线程已推出。
大多数可阻塞的库函数都只是抛出InterruptedException作为中断响应:尽快推出执行流程,并把中断消息传递给调用者,从而使调用栈中的上层代码可以采取进一步的操作。
当调用可中断的阻塞方法,例如sleep和wait,有两种策略可以用于处理InterruptedException:
- 传递异常(可能在执行某个 特定于任务的清理后),从而使你的方法也能成为可中断的阻塞方法
- 恢复中断状态,从而使调用栈中的上层代码能够对其进行处理
通过Future来实现取消:
ExecutorService.submit将返回一个Future来描述任务。Future拥有一个cancel方法,该方法带有一个boolean类型的参数mayInterruptIfRunning,表示操作能否接受中断,如果为true并且任务正在某个线程中运行,那么这个线程能被中断,如果为false,那么意味着“若任务还没有启动,就不要启动它”。
当尝试取消某个任务时,不宜直接中断线程池,因为你并不知道当中断请求到达时正在运行什么任务----只能通过任务的Future来实现取消。
4.处理不可中断的阻塞
并非所有的可阻塞方法或阻塞机制都能响应中断,如果一个线程由于执行同步的Socket I/O或者等待获得内置锁而阻塞,那么中断请求只能设置线程的中断状态,除此之外没有其他任何作用。对于那些由于执行不可中断操作而被阻塞的线程,可以使用类似于中断的手段来停止这些线程,但这要求我们必须知道线程阻塞的原因。
5.停止基于线程的服务
应用程序通常会创建拥有多个线程的服务,例如线程池,并且这些服务的生命周期通常比创建它们的方法的生命周期更长。如果应用程序准备退出,那么这些服务所拥有的线程也需要结束。由于无法通过抢占式的方法来停止线程,因此它们需要自行结束。
正确的封装原则是:除非拥有某个线程,否则不能对该线程进行操控,例如:中断线程或者修改线程的优先级等。对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么就应该提供生命周期方法。
线程的所有权是不可传递的:应用程序可以拥有服务,服务也可以拥有工作者线程,但应用程序并不能拥有工作者线程,因此应用程序不能直接停止工作者线程。
6.处理非正常的线程终止
导致线程提前死亡的最主要原因就是RuntimeException,由于这些异常表示出现了某种编程错误或者其他不可修复的错误,因此它们通常不会被捕获,它们不会在调用栈中逐层传递,而是默认地在控制台中输出栈追踪信息,并终止线程。
除了主动捕获异常,在Thread API中提供了UncaughtExceptionHandler,它能检测出某个线程由于未捕获的异常而终结的情况。当一个线程由于未捕获异常而退出时,JVM会把这个事件报告给应用程序提供的UncaughtExceptionHandler,如果没有提供任何异常处理器,那么默认的行为是将栈追踪信息输出到System.err。
public interface UncaughtExceptionHandler {
void uncaughtException(Thread t, Throwable e);
}
在运行时间较长的应用程序中,通常会为所有线程的未捕获异常指定同一个异常处理器,并且该处理器至少会将异常信息记录到日志中。
7.JVM关闭
JVM既可以正常关闭,也可以强行关闭。正常关闭的触发方式包括:
- 当最后一个普通线程结束时(线程分为普通线程和守护线程)
- 调用了System.exit
- 通过其他特定于平台的方法关闭时(例如发送了SIGINT信号或键入Ctrl-C)
强行关闭JVM的方法:
调用Runtime.halt、在操作系统中kill JVM
1. 关闭钩子
关闭钩子(Shutdown Hook)是指通过Runtime.addShutdownHook注册的但尚未开的线程。
在正常关闭中,JVM首先调用所有已注册的关闭钩子,但JVM并不能保证关闭钩子的调用顺序,在关闭应用程序线程时,如果有线程(包括守护线程)仍然在运行,那么这些线程接下来将与关闭进程并发执行,当所有的关闭钩子都执行结束时,如果runFinalizersOnExit为true,那么JVM将运行终结器,然后再停止。如果关闭钩子或终结器没有执行完成,那么正常关闭进程“挂起”并且JVM必须被强行关闭。当被强行关闭时,只是关闭JVM,而不会运行关闭钩子。
关闭钩子不应该依赖那些可能被应用程序或其他关闭钩子关闭的服务,实现这种功能的一种方式是对所有服务使用同一个关闭钩子而不是每个服务使用一个不同的关闭钩子。
一个关闭钩子的例子:
public void start() {
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
try {
LogService.this.stop();
catch (InterruptedException ignored) {}
}
}
});