0%

两阶段终止模式:优雅的终止线程

Thread-Per-Message模式与Worker Thread模式,是从技术角度,启动多线程去执行一个异步任务。线程执行完成或者出现异常就会进入终止状态。(如何在线程T1中优雅的终止线程T2)。

Java语言的Thread类中曾经提供了一个stop()方法,用来终止线程,但是这个方法太粗暴,被终止的线程没有机会料理后事。

两阶段终止模式

将终止过程分成两个阶段,其中第一个阶段主要是线程T1向线程T2发送终止指令,而第二阶段是线程T2响应终止指令

Java线程进入终止状态的前提是线程进入Runnable状态,而实际上线程也可能出在休眠状态,想要终止一个线程,首先要把线程的状态从休眠状态转换到Runnable状态。这个要靠Java Thread类提供的interrupt()方法,它可以将休眠状态的线程转换到Runnable状态。

线程转换到Runnable状态之后,优雅的方式是让线程自己执行完run()方法来转换到终止状态。一般采用的方法是设置一个标志位,然后线程会在合适的时机检查这个标志位,如果发现符合终止条件,则自动退出run()方法。这个过程就是第二阶段:响应终止指令。而终止指令,包括两方面内容:interrupt()方法和线程终止的标志位。

用两阶段终止模式终止监控操作

实际工作中,有些监控系统需要动态的采集一些数据,一般都是监控系统发送采集指令给被监控系统的监控代理,监控代理接收到指令之后,从监控目标收集数据,然后回传给监控系统。有些监控项对系统性能影响很大,出于对性能的考虑,不能一直持续监控,所以动态采集功能一般都会有终止操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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) {

}
}
});
//执行到此处说明线程马上终止
rptThread.start();
}
//终止采集功能
synchronized void stop() {
...
}
}

start()方法会启动一个新的线程rptThread来执行监控数据采集和回传的功,stop()方法需要优雅的终止线程rptThread。
按照两阶段终止模式,首先需要做的是将线程rptThread状态转换到Runnable,只需要在stop()方法中调用rptThread.interrupt()就可以了。
如果将线程的中断状态:Thread.currentThread().isInterrupted()作为标志位。在捕获Thread.sleep()的中断异常之后,需要通过Thread.currentThread().interrupt()重新设置线程的中断状态,因为JVM的异常处理会清除线程的中断状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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() {
//Thread类的interrupt()方法,将休眠状态的线程转换到Runnable状态。
rptThread.interrupt();
}
}

在线程的run()方法中很可能调用第三方类库提供的方法,而没有办法保证第三方类库正确处理了线程的中断异常,如果第三方类库在捕获到Thread.sleep()方法抛出的中断异常后,没有重新设置线程的中断状态,那么就会导致线程不能正常终止。所以强烈建议设置自己的线程终止标志位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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,
//(已经设置了中断标志位,这里只是为了将线程从休眠状态转换到Runnable状态)
rptThread.interrupt();
}
}

使用isTerminated作为线程终止标志位,此时无论是否正确处理了线程的中断异常,都不会影响线程优雅的终止。

线程Runnable状态时,无法响应中断,是通过系统函数如sleep等响应中断的。

优雅的终止线程池

线程池提供了两个方法:shutdown()和shutdownNow()。

线程池的实现原理:Java线程池是生产者-消费者模式的一种实现,提交给线程池的任务,首先是进入一个阻塞队列中,之后线程池中的线程从阻塞队列中取出任务执行。

shutdown()方法是一种很保守的关闭线程池的方法。线程池执行shutdown()后,就会拒绝接收新的任务,但是会等待线程池中正在执行的任务和已经进入阻塞队列的任务都执行完之后才最终关闭线程池。

showdownNow()方法相对激进一些,线程池执行shutdownNow()后,会拒绝接收新的任务,同时还会中断线程池中正在执行的任务,已经进入阻塞队列的任务也被剥夺了执行的机会,不过这些被剥夺执行机会的任务会作为shutdownNow()方法的返回值返回。因为shutdownNow()方法会中断正在执行的线程,所以提交到线程池的任务,如果需要优雅的结束,就需要正确的处理线程中断。

如果提交到线程池的任务不允许取消,那就不能使用shutdownNow()方法终止线程池。如果提交到线程池的任务允许后续以补偿的方式重新执行,则可以使用。将已提交但尚未开始执行的任务以及已经取消的正在执行的任务保存起来,以便后续重新执行。(Java并发编程实战-7.2.5shutdownNow的局限性P130)。

shutdown()和shutdownNow()实质上使用的也是两阶段终止模式,只是终止指令的范围不同而已,前者只影响阻塞队列接收任务,后者范围扩大到线程池中所有的任务。

总结

两阶段终止模式是一种应用广泛的并发设计模式,在Java语言中使用两阶段终止模式来优雅的终止线程,需要注意两个关键点:一个是仅检查终止标志位是不够的,因为线程的状态可能处于休眠状态;另一个是仅检查线程的中断状态也是不够的,因为依赖的第三方类库很可能没有正确处理中断异常。

当使用Java的线程池来管理线程的时候,需要依赖线程池提供的shutdown()和shutdownNow()方法来终止线程池。在使用时需要注意它们的应用场景。