第三部分-并发设计模式35:两阶段终止模式
1.两阶段终止模式
如何优雅的终止线程?
2.线程执行完或者出现异常就会进入终止状态
现在要思考的是,不是自己终止自己,而是在线程T1中终止线程T2
而且是优雅的终止,给T2线程一个料理后事的机会,而不是被一剑封喉
3.历史
java的Thread类提供了一个stop方法,用来终止线程,但早已不适用
stop方法就是一剑封喉方法,被终止的线程没有机会料理后事
4.两阶段终止模式示意图
终止过程分为2类
1.主线程T1向T2发送终止指令
2.线程T2响应终止指令
5.线程状态生命周期
6.说明
(1)切换到RUNNABLE状态
线程进入终止状态的前提是线程进入RUNNABLE状态,而可能线程目前在休眠状态。
如果我们要终止一个线程,必须要把休眠状态切回RUNNABLE状态
依赖Thread类的interrupt()方法,可以将休眠状态线程切换为RUNNABLE状态
(2)RUNNABLE状态如何再终止
优雅的方式是让线程自己执行完run方法。所以一般采取的策略是设置标志位。线程在合适时机检查标志位,符合条件,自动退出run方法。(响应终止指令)
两阶段终止其实就是:interrupt方法及线程终止的标志位
7.用两阶段终止监控操作
监控系统,动态收集数据场景,监控端发送指令给被监控代理,代理收到指令,从目标收集数据,回传监控端。动态采集也会有终止操作
伪代码
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(){
//如何实现?
}
}
代码整改,添加两阶段终止模式,切换线未RUNNABLE状态,选择标志位,适当时机退出线程
伪代码
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();
}
}
8.思考上述场景,是否有优化空间
run方法中如果调用第三方类库,没有办法保证第三方类库也处理了线程中断异常。如果第三方类库捕获到中断异常后,没有重新设置线程的中断状态(Thread.currentThread().interrupt()),那么就不能保证线程正常终止!(Thread.currentThread().isInterrupted()状态不变,退不出死循环)。如何优化呢?设置自己的终止标志位,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();
}
}
为什么最后不仅仅修改状态位,还要执行rptThread.interrupt()呢?那是因为线程rptThread不一定刚好在RUNNABLE状态,如果是WAITING状态就无法关闭
9.关闭线程池
上面说了那么多,单独线程,其实还是使用场景很少,java的线程创建销毁,成本高。而使用更多的还是java的线程池技术
那么如何关闭线程池呢?
- shutdown:线程池拒绝接受新任务,同时等待线程池中正在执行的任务和已经进入阻塞队列的任务都执行完才会最终关闭线程池
- shutdownNow:拒绝接受新的任务,同时还会中断线程池中正在执行的任务,已经进入阻塞队列的任务也被剥夺执行的机会。被剥夺执行机会的任务会作为shutdownNow()方法的返回值返回。
关闭线程池用的也是两阶段终止模式,只是指令的范围略有不同,一个影响阻塞队列接受任务,一个是线程池中的所有任务。
生产中是如何关闭线程池的
伪代码
public void close() {
masterExecService.shutdown();
boolean terminated = false; //标记位
try {
// sleep 5 second,return threadPool status,close return true ,else return false
terminated = masterExecService.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException ignore) {
Thread.currentThread().interrupt();
}
if(!terminated){
logger.warn("masterExecService shutdown without terminated, increase await time");
}
logger.info("master schedule service stopped...");
}
Application.java中添加ShutdownHook钩子
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
if (Stopper.isRunning()) {
close();
}
}
}));
10.总结
两阶段终止模式,很广泛的并发设计模式。
仅仅检查标志位是不够的,线程状态可能是休眠。仅仅检查中断状态也是不够,第三方不会处理中断异常