You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

198 lines
11 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 35 | 两阶段终止模式:如何优雅地终止线程?
前面两篇文章我们讲述的内容,从纯技术的角度看,都是**启动**多线程去执行一个异步任务。既启动,那又该如何终止呢?今天咱们就从技术的角度聊聊如何优雅地**终止**线程,正所谓有始有终。
在[《09 | Java线程Java线程的生命周期》](https://time.geekbang.org/column/article/86366)中我曾讲过线程执行完或者出现异常就会进入终止状态。这样看终止一个线程看上去很简单啊一个线程执行完自己的任务自己进入终止状态这的确很简单。不过我们今天谈到的“优雅地终止线程”不是自己终止自己而是在一个线程T1中终止线程T2这里所谓的“优雅”指的是给T2一个机会料理后事而不是被一剑封喉。
Java语言的Thread类中曾经提供了一个stop()方法,用来终止线程,可是早已不建议使用了,原因是这个方法用的就是一剑封喉的做法,被终止的线程没有机会料理后事。
既然不建议使用stop()方法那在Java领域我们又该如何优雅地终止线程呢
## 如何理解两阶段终止模式
前辈们经过认真对比分析,已经总结出了一套成熟的方案,叫做**两阶段终止模式**。顾名思义就是将终止过程分成两个阶段其中第一个阶段主要是线程T1向线程T2**发送终止指令**而第二阶段则是线程T2**响应终止指令**。
![](https://static001.geekbang.org/resource/image/a5/5c/a5ea3cb2106f11ef065702f34703645c.png)
两阶段终止模式示意图
那在Java语言里终止指令是什么呢这个要从Java线程的状态转换过程说起。我们在[《09 | Java线程Java线程的生命周期》](https://time.geekbang.org/column/article/86366)中曾经提到过Java线程的状态转换图如下图所示。
![](https://static001.geekbang.org/resource/image/3f/8c/3f6c6bf95a6e8627bdf3cb621bbb7f8c.png)
Java中的线程状态转换图
从这个图里你会发现Java线程进入终止状态的前提是线程进入RUNNABLE状态而实际上线程也可能处在休眠状态也就是说我们要想终止一个线程首先要把线程的状态从休眠状态转换到RUNNABLE状态。如何做到呢这个要靠Java Thread类提供的**interrupt()方法**它可以将休眠状态的线程转换到RUNNABLE状态。
线程转换到RUNNABLE状态之后我们如何再将其终止呢RUNNABLE状态转换到终止状态优雅的方式是让Java线程自己执行完 run() 方法,所以一般我们采用的方法是**设置一个标志位**然后线程会在合适的时机检查这个标志位如果发现符合终止条件则自动退出run()方法。这个过程其实就是我们前面提到的第二阶段:**响应终止指令**。
综合上面这两点,我们能总结出终止指令,其实包括两方面内容:**interrupt()方法**和**线程终止的标志位**。
理解了两阶段终止模式之后,下面我们看一个实际工作中的案例。
## 用两阶段终止模式终止监控操作
实际工作中,有些监控系统需要动态地采集一些数据,一般都是监控系统发送采集指令给被监控系统的监控代理,监控代理接收到指令之后,从监控目标收集数据,然后回传给监控系统,详细过程如下图所示。出于对性能的考虑(有些监控项对系统性能影响很大,所以不能一直持续监控),动态采集功能一般都会有终止操作。
![](https://static001.geekbang.org/resource/image/11/5f/11e3b0a4a9cf743124091b22e10d275f.png)
动态采集功能示意图
下面的示例代码是**监控代理**简化之后的实现start()方法会启动一个新的线程rptThread来执行监控数据采集和回传的功能stop()方法需要优雅地终止线程rptThread那stop()相关功能该如何实现呢?
```
class Proxy {
boolean started = false;
//采集线程
Thread rptThread;
//启动采集功能
synchronized void start(){
//不允许同时启动多个采集线程
if (started) {
return;
}
started = true;
rptThread = new Thread(()->{
while (true) {
//省略采集、回传实现
report();
//每隔两秒钟采集、回传一次数据
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
//执行到此处说明线程马上终止
started = false;
});
rptThread.start();
}
//终止采集功能
synchronized void stop(){
//如何实现?
}
}
```
按照两阶段终止模式我们首先需要做的就是将线程rptThread状态转换到RUNNABLE做法很简单只需要在调用 `rptThread.interrupt()` 就可以了。线程rptThread的状态转换到RUNNABLE之后如何优雅地终止呢下面的示例代码中我们选择的标志位是线程的中断状态`Thread.currentThread().isInterrupted()` 需要注意的是我们在捕获Thread.sleep()的中断异常之后,通过 `Thread.currentThread().interrupt()` 重新设置了线程的中断状态因为JVM的异常处理会清除线程的中断状态。
```
class Proxy {
boolean started = false;
//采集线程
Thread rptThread;
//启动采集功能
synchronized void start(){
//不允许同时启动多个采集线程
if (started) {
return;
}
started = true;
rptThread = new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
//省略采集、回传实现
report();
//每隔两秒钟采集、回传一次数据
try {
Thread.sleep(2000);
} catch (InterruptedException e){
//重新设置线程中断状态
Thread.currentThread().interrupt();
}
}
//执行到此处说明线程马上终止
started = false;
});
rptThread.start();
}
//终止采集功能
synchronized void stop(){
rptThread.interrupt();
}
}
```
上面的示例代码的确能够解决当前的问题但是建议你在实际工作中谨慎使用。原因在于我们很可能在线程的run()方法中调用第三方类库提供的方法而我们没有办法保证第三方类库正确处理了线程的中断异常例如第三方类库在捕获到Thread.sleep()方法抛出的中断异常后,没有重新设置线程的中断状态,那么就会导致线程不能够正常终止。所以强烈建议你**设置自己的线程终止标志位**例如在下面的代码中使用isTerminated作为线程终止标志位此时无论是否正确处理了线程的中断异常都不会影响线程优雅地终止。
```
class Proxy {
//线程终止标志位
volatile boolean terminated = false;
boolean started = false;
//采集线程
Thread rptThread;
//启动采集功能
synchronized void start(){
//不允许同时启动多个采集线程
if (started) {
return;
}
started = true;
terminated = false;
rptThread = new Thread(()->{
while (!terminated){
//省略采集、回传实现
report();
//每隔两秒钟采集、回传一次数据
try {
Thread.sleep(2000);
} catch (InterruptedException e){
//重新设置线程中断状态
Thread.currentThread().interrupt();
}
}
//执行到此处说明线程马上终止
started = false;
});
rptThread.start();
}
//终止采集功能
synchronized void stop(){
//设置中断标志位
terminated = true;
//中断线程rptThread
rptThread.interrupt();
}
}
```
## 如何优雅地终止线程池
Java领域用的最多的还是线程池而不是手动地创建线程。那我们该如何优雅地终止线程池呢
线程池提供了两个方法:**shutdown()**和**shutdownNow()**。这两个方法有什么区别呢?要了解它们的区别,就先需要了解线程池的实现原理。
我们曾经讲过Java线程池是生产者-消费者模式的一种实现,提交给线程池的任务,首先是进入一个阻塞队列中,之后线程池中的线程从阻塞队列中取出任务执行。
shutdown()方法是一种很保守的关闭线程池的方法。线程池执行shutdown()后,就会拒绝接收新的任务,但是会等待线程池中正在执行的任务和已经进入阻塞队列的任务都执行完之后才最终关闭线程池。
而shutdownNow()方法相对就激进一些了线程池执行shutdownNow()后会拒绝接收新的任务同时还会中断线程池中正在执行的任务已经进入阻塞队列的任务也被剥夺了执行的机会不过这些被剥夺执行机会的任务会作为shutdownNow()方法的返回值返回。因为shutdownNow()方法会中断正在执行的线程,所以提交到线程池的任务,如果需要优雅地结束,就需要正确地处理线程中断。
如果提交到线程池的任务不允许取消那就不能使用shutdownNow()方法终止线程池。不过如果提交到线程池的任务允许后续以补偿的方式重新执行也是可以使用shutdownNow()方法终止线程池的。[《Java并发编程实战》](time://mall?url=https%3A%2F%2Fh5.youzan.com%2Fv2%2Fgoods%2F2758xqdzr6uuw)这本书第7章《取消与关闭》的“shutdownNow的局限性”一节中提到一种将已提交但尚未开始执行的任务以及已经取消的正在执行的任务保存起来以便后续重新执行的方案你可以参考一下方案很简单这里就不详细介绍了。
其实分析完shutdown()和shutdownNow()方法你会发现,它们实质上使用的也是两阶段终止模式,只是终止指令的范围不同而已,前者只影响阻塞队列接收任务,后者范围扩大到线程池中所有的任务。
## 总结
两阶段终止模式是一种应用很广泛的并发设计模式在Java语言中使用两阶段终止模式来优雅地终止线程需要注意两个关键点一个是仅检查终止标志位是不够的因为线程的状态可能处于休眠态另一个是仅检查线程的中断状态也是不够的因为我们依赖的第三方类库很可能没有正确处理中断异常。
当你使用Java的线程池来管理线程的时候需要依赖线程池提供的shutdown()和shutdownNow()方法来终止线程池。不过在使用时需要注意它们的应用场景尤其是在使用shutdownNow()的时候,一定要谨慎。
## 课后思考
本文的示例代码中线程终止标志位isTerminated被声明为volatile你觉得是否有必要呢
```
class Proxy {
//线程终止标志位
volatile boolean terminated = false;
......
}
```
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。