启动一个任务和线程很容易,但是想要他们安全快速可靠地停止下来就需要花一点功夫了. Java没有提供任何机制来安全的终止线程. 但是它提供了一种中断机制(interruption), 这是一种协作机制, 能够使一个线程终止另一个线程的当前工作.
7.1 任务取消
如果外部代码能够在某个操作完成之前将其置入"完成"状态, 那么这个操作就可以称为可取消的(cancelled). 取消的原因有:
用户请求取消: 用户点击取消按钮.
有时间限制的操作: 超时等问题导致的取消.
应用程序事件: 比如应用程序对某个问题空间进行了分解并搜索, 从而使不同的任务可以搜索问题,当某一个任务找到了解决方法,那么其他的任务都可以取消了.
错误
关闭: 程序或服务关闭.
一个可取消的任务必须拥有取消策略(cancellation Policy), 这个策略中将详细地定义取消操作的"how如何取消","when何时取消","what取消该做哪些操作".
7.1.1 中断
每一个线程都一个Boolean类型的中断状态. 当中断线程时, 这个线程的中断状态将被设置为true. Thread类中有一些与中断有关的方法.
public class Thread {
// 该方法可以中断目标线程
public void interrupt() {...}
// 该方法可以返回目标线程的中断状态
public boolean isInterrupted() {...}
// 该方法可以清除当前线程的中断状态, 并返回它之前的值, 也是清除状态状态的唯一方法
public static boolean interrupted() {...}
}
阻塞库方法, 比如 Thread.sleep(), Object.wait()等, 都会检查线程何时中断,并且在发现中断时提前返回. 它们在响应中断时的操作包括: 清除中断状态, 抛出 InterruptException 中断异常,表示阻塞操作由于中断异常而提前结束.
对中断操作的正确理解是: 它并不会真正的中断一个正在运行的线程, 而只是发出中断请求, 然后由线程在下一个合适的时刻中断自己(这些时刻也被称为取消点). 比如 wait,sleep,join等,这些方法严格的处理这样的请求.
> 通常, 中断时取消的最合理方式.
public PrimeProducer extends Thread {
private final BlockingQueue queue;
...
public void run(){
try{
BigInterge p = BigInterger.ONE;
while(Thread.currentThread.isInterrupted()){
queue.put(p = p.nextProbablePrime());
}
}catch(InterruptedException e){
// 允许线程退出
}
}
public void cancel(){
this.interrupt();
}
}
7.1.2 中断策略
最合理的中断策略是某种形式的线程级取消操作或服务级取消操作: 尽快退出, 在必要时进行清理,通知某个所有者该线程已经退出. 此外还可以建立其他的中断策略, 比如暂停服务或重启.
7.1.3 响应中断
当调用可中断的阻塞函数时,例如Thread.sleep, 或 BlockQueue.put等, 有两种实用的策略处理InterruptedException:
传递异常(可能在执行某个特定任务的清除操作之后), 从而使你的方法也成为可中断的阻塞方法.
恢复中断状态, 从而使调用栈中的上层代码能够对其进行处理.
> 只有实现了线程中断策略的代码才可以屏蔽中断请求. 在常规的任务和库代码中都不应该屏蔽中断请求.
7.1.5 通过 Future 来实现取消
Future是一种抽象的机制, 用来管理任务的生命周期, 处理异常,实现取消等相关操作. 它有一个 cancel 方法, 此方法带有一个Boolean类型的参数 mayInterruptIfRunning, 参数的意义是任务是否能够接收中断,而不是表示任务是否能检测并处理中断.
执行任务的线程是由标准的 Exector 创建的Future, 它实现了一种中断策略使得任务可以通过中断被取消, 所以这样的Future可以调用cancel并且传递 mayInterruptIfRunning=true.
> Future.get 抛出 InterruptedException 或 TimeOutException 时,如果知道不再需要结果,那么可以调用 Future.cancel 来取消任务.
7.1.6 处理不可中断的阻塞
以下是 不可中断的阻塞的情况:
Java.io 包中的同步 socket I/O
Java.io 包中的同步 I/O
Selector 的异步 I/O
获取某个锁
如果仅仅是通过设置线程的中断状态,那么将起不到任何用处. 对于这些由于执行不可中断操作而被阻塞的线程, 需要我们特殊的处理让其停止. 比如重写 interrupt方法, 增加关闭资源操作.
7.1.7 采用 newTaskFor 来封装非标准的取消
通过 ThreadPoolExecutor.newTaskFor 方法创建一个 RunnableFuture 任务.
7.2 停止基于线程的服务
主要是说线程池. 线程池提供了生命周期方法.
7.2.2 关闭 ExecutorService
ExecutorService 提供了shutdown正常关闭和shutdownNow强制关闭两种终止服务的方法. 强制关闭时会首先关闭正在运行的任务, 然后返回所有尚未启动的任务清单.
使用 shutdown时可以配合 awaitTermination 方法一起使用.
ExecutorService exec = ...
public void stop(){
exec.shutdown();
// 可以阻塞等待
exec.awaitTermination(TIMEOUT, UNIT);
}
7.2.3 "毒丸"对象
另一种关闭生产者-消费者的方式是使用"毒丸(Posion Pill)"对象: "毒丸"指一个放在队列上的对象, 其含义是: "当得到这个对象时,立即停止". 在FIFO(先进先出)队列中, "毒丸"对象将确保消费者在关闭之前先完成队列中的所有工作,在提交"毒丸"对象之前提供的所有工作都被处理, 而生产者在提交"毒丸"对象后, 将不会再提交任何工作.
// 生产者线程
public class CrawlerThread extends Thread {
public void run(){
try{
crawl(root);
}catch(InterruptedException e){
/* 发生异常 */
} finally {
while(true){
try{
queue.put(POSION);
break;
} catch (InterruptedException e){
/* 重新尝试 */
}
}
}
}
private void crawl(File root){...}
}
// 消费者线程
public class IndexerThread extends Thread {
public void run(){
try{
while(true){
File file = queue.take();
if(file == POSION){
break;
}else{
indexFile(file);
}
}
} catch (InterruptedException e){
// 退出线程
}
}
}
7.2.5 shutdownNow 的局限性
shutdownNow 强制关闭 ExecutorExecutor 时会尝试取消正在执行的任务, 并返回所有已提交但尚未开始的任务. 然后我们无法通过常规的方法知道哪些任务已经开始但是未结束.
public class TrackingExecutor extends AbstractExecutorService {
private ExecutorService exec;
private final Set tasksCancelledAtShutdown = new CopyOnWriteArraySet();
public List getCancelledTasks(){
if(!exec.isTerminated()){
throw new IllegalStateException(...);
}
return new ArrayList(tasksCancelledAtShutdown);
}
public void execute(final Runnable runnbale){
exec.execute(new Runnable(){
public void run(){
try{
runnable.run();
} finally {
// 判断当前线程池是否关闭并且当前线程是否中断
if(isShutdown() && Thread.currentThread.isInterrupted()){
tasksCancelledAtShutdown.add(runnbale);
}
}
}
});
}
}
7.3 处理非正常的线程终止
典型的线程池工作线程的结构, 如果任务抛出一个未检查任务, 那么它将使线程终结,但会首先通知框架该线程已经终结. 然后框架可能会用新的线程来代替这个工作线程. 也可能不会. 以下代码通过主动方法来解决未检查异常.
// 典型的线程池工作线程的结构
public void run(){
Throwable thrown = null;
try{
while(!isInterrupted()){
runTask(getTaskFromWorkQueue());
}
} catch (Throwable e) {
thrown = e;
} finally {
threadExited(this, thrown);
}
}
未捕获异常的处理
Thread 可以设置 UncaughtExceptionHandler,来检查线程由于未捕获的异常而终结的情况. 可以将上述的主动获取异常方法与此方法结合使用,有效防止线程泄露问题.
7.4 JVM 关闭
JVM 可以正常关闭,也可以强制关闭. 正常的触发关闭包括: 当最后一个线程(正常的线程,非守护线程)结束时, 或者调用了System.exit时, 或者通过其他特定与平台的方法关闭时(例如发送 SIGINT 信号或者键入 Ctrl-C). 也可以通过调用Runtime.halt或者在操作系统中"杀死"JVM进程(例如发送SIGKILL)来强制关闭JVM.
7.4.1 关闭钩子
在正常关闭中, JVM 首先调用所有已注册的关闭钩子(shutdown hook). 关闭钩子是指通过 Runtime.addShutdownHook 注册的但尚未开始的线程.
JVM 关闭的流程: (守护与非守护)仍然在运行中的线程与关闭进程并发执行, 当所有的关闭钩子都执行完毕时, runFinalizerOnExit 为 ture, 那么JVM 将运行终结器, 然后再停止.
注意: 由于关闭钩子会并发执行,所以不应该依赖那些可能被应用程序或其他关闭钩子关闭的服务. 即对所有服务使用同一个关闭钩子, 并且在关闭钩子中串行的执行一些列的关闭操作.
7.4.2 守护线程
线程分为两种: 普通线程和守护线程. 在 JVM 启动时创建的所有线程,除了主线程以外, 其他的线程都是守护线程(例如垃圾回收器以及其他执行辅助工作的线程). 当创建一个新线程时, 新线程会继承创建它的线程的守护状态, 因此在默认情况下, 主线程创建的所有线程都是普通线程.
普通线程和守护线程的差异仅在于当前线程退出时发生的额操作. 当一个线程退出时, JVM 会检查其他正在运行的线程, 如果这些线程都是守护线程, 那么 JVM 会正常的退出操作. 当 JVM 停止时, 所有仍然存在的守护线程都将会被抛弃----既不会执行finally代码块, 也不会执行回卷栈, 而 JVM 只是直接退出.
7.4.3 终结器
有些资源比如句柄或套接字句柄, 当不再需要它们时必须显示的交还给操作系统, 垃圾回收器对那些定义了 finalize 方法的对象会进行特殊的处理: 在回收器释放它们后,调用它们的 finalize 方法,从而保证了一些持久化的资源被释放.