话题:两阶段终止模式:如何优雅地终止线程?
启动多线程去执行一个异步任务。既启动,那又该如何终止呢?今天咱们就从技术的角度聊聊如何优雅地终止线程,正所谓有始有终。
线程执行完或者出现异常就会进入终止状态。这样看,终止一个线程看上去很简单啊!一个线程执行完自己的任务,自己进入终止状态,这的确很简单。不过我们今天谈到的“优雅地终止线程”,不是自己终止自己,而是在一个线程T1中,终止线程T2;这里所谓的“优雅”,指的是给T2一个机会料理后事,而不是被一剑封喉。
Java语言的Thread类中曾经提供了一个stop()方法,用来终止线程,可是早已不建议使用了,原因是这个方法用的就是一剑封喉的做法,被终止的线程没有机会料理后事。
既然不建议使用stop()方法,那在Java领域,我们又该如何优雅地终止线程呢?
如何理解两阶段终止模式
前辈们经过认真对比分析,已经总结出了一套成熟的方案,叫做两阶段终止模式。顾名思义,就是将终止过程分成两个阶段,其中第一个阶段主要是线程T1向线程T2发送终止指令,而第二阶段则是线程T2响应终止指令。
那在Java语言里,终止指令是什么呢?这个要从Java线程的状态转换过程说起。之前曾经提到过Java线程的状态转换图,如下图所示。
从这个图里你会发现,Java线程进入终止状态的前提是线程进入RUNNABLE状态,而实际上线程也可能处在休眠状态,也就是说,我们要想终止一个线程,首先要把线程的状态从休眠状态转换到RUNNABLE状态。如何做到呢?这个要靠Java Thread类提供的interrupt()方法,它可以将休眠状态的线程转换到RUNNABLE状态。
线程转换到RUNNABLE状态之后,我们如何再将其终止呢?RUNNABLE状态转换到终止状态,优雅的方式是让Java线程自己执行完 run() 方法,所以一般我们采用的方法是设置一个标志位,然后线程会在合适的时机检查这个标志位,如果发现符合终止条件,则自动退出run()方法。这个过程其实就是我们前面提到的第二阶段:响应终止指令。
综合上面这两点,我们能总结出终止指令,其实包括两方面内容:interrupt()方法和线程终止的标志位。
理解了两阶段终止模式之后,下面我们看一个实际工作中的案例。
用两阶段终止模式终止监控操作
实际工作中,有些监控系统需要动态地采集一些数据,一般都是监控系统发送采集指令给被监控系统的监控代理,监控代理接收到指令之后,从监控目标收集数据,然后回传给监控系统,详细过程如下图所示。出于对性能的考虑(有些监控项对系统性能影响很大,所以不能一直持续监控),动态采集功能一般都会有终止操作。
下面的示例代码是监控代理简化之后的实现,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并发编程实战》这本书第7章《取消与关闭》的“shutdownNow的局限性”一节中,提到一种将已提交但尚未开始执行的任务以及已经取消的正在执行的任务保存起来,以便后续重新执行的方案,你可以参考一下,方案很简答,这里就不详细介绍了。
其实分析完shutdown()和shutdownNow()方法你会发现,它们实质上使用的也是两阶段终止模式,只是终止指令的范围不同而已,前者只影响阻塞队列接收任务,后者范围扩大到线程池中所有的任务。
总结
两阶段终止模式是一种应用很广泛的并发设计模式,在Java语言中使用两阶段终止模式来优雅地终止线程,需要注意两个关键点:一个是仅检查终止标志位是不够的,因为线程的状态可能处于休眠态;另一个是仅检查线程的中断状态也是不够的,因为我们依赖的第三方类库很可能没有正确处理中断异常。
当你使用Java的线程池来管理线程的时候,需要依赖线程池提供的shutdown()和shutdownNow()方法来终止线程池。不过在使用时需要注意它们的应用场景,尤其是在使用shutdownNow()的时候,一定要谨慎。
Demo1
public class CounterTest {//多线程Two Phase Termination设计模式 2 阶段 终止
public static void main(String[] args) throws InterruptedException {
CounterIncrement counterIncrement = new CounterIncrement();
counterIncrement.start();
Thread.sleep(10_000L);
counterIncrement.close();
}
}
public class CounterIncrement extends Thread {
private volatile boolean terminated = false;
private int counter = 0;
private Random random = new Random(System.currentTimeMillis());
@Override
public void run() {
try {
while (!terminated) {
System.out.println(Thread.currentThread().getName() + " " + counter++);
Thread.sleep(random.nextInt(1000));
}
} catch (InterruptedException e) {
//e.printStackTrace();
} finally {
this.clean();
}
}
private void clean() {
System.out.println("do some clean work for the second phase,current counter " + counter);
}
public void close() {
this.terminated = true;
this.interrupt();
}
}
1
public class AppServerClient {
public static void main(String[] args) throws InterruptedException, IOException {
AppServer server = new AppServer(13345);
server.start();
Thread.sleep(15_000L);
server.shutdown();
}
}
2
public class AppServer extends Thread {
private final int port;
private static final int DEFAULT_PORT = 12722;
private volatile boolean start = true;
private List<ClientHandler> clientHandlers = new ArrayList<>();
private final ExecutorService executor = Executors.newFixedThreadPool(10);
private ServerSocket server;
public AppServer() {
this(DEFAULT_PORT);
}
public AppServer(int port) {
this.port = port;
}
@Override
public void run() {
try {
this.server = new ServerSocket(port);
while (start) {
//接受请求
Socket client = server.accept();
//将socket再次封装成ClientHandler对象
ClientHandler clientHandler = new ClientHandler(client);
executor.submit(clientHandler);
this.clientHandlers.add(clientHandler);
}
} catch (IOException e) {
// throw new RuntimeException(e);
} finally {
//像这种资源的东西都不要忘记资源的释放
this.dispose();
}
}
private void dispose() {
System.out.println("dispose");
this.clientHandlers.stream().forEach(ClientHandler::stop);
this.executor.shutdown();
}
public void shutdown() throws IOException {
this.start = false;
this.interrupt();
this.server.close();
}
}
3
public class ClientHandler implements Runnable {
private final Socket socket;
private volatile boolean running = true;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
PrintWriter printWriter = new PrintWriter(outputStream)) {
while (running) {
String message = br.readLine();
if (message == null)
break;
System.out.println("Come from client >" + message);
printWriter.write("echo " + message + "\n");
printWriter.flush();
}
} catch (IOException e) {
// e.printStackTrace();
this.running = false;
} finally {
this.stop();
}
}
public void stop() {
if (!running) {
return;
}
this.running = false;
try {
this.socket.close();
} catch (IOException e) {
//
}
}
}
Demo2
1
public class ThreadCloseGraceful {//设置标志位的方式结束线程的生命周期
private static class Worker extends Thread {
private volatile boolean start = true;
@Override
public void run() {
while (start) {
//
}
}
public void shutdown() {
this.start = false;
}
}
public static void main(String[] args) {
Worker worker = new Worker();
worker.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
worker.shutdown();
}
}
2
public class ThreadCloseGraceful2 {//采用优雅的方式结束线程生命周期
private static class Worker extends Thread {
@Override
public void run() {
while (true) {
//方式2:用线程自带的打断标记位进行判断
System.out.println("Before Thread.interrupted() invoke: " + this.isInterrupted());//false
if (Thread.interrupted()) {//该api会清除标志位的flag,上下2句打印状态都是false 静态方法
System.out.println("After Thread.interrupted() invoke: " + this.isInterrupted());//false 实例方法
break;
}
//方式1:用小睡一下然会在外面打断它即可
//有await,sleep,join等方法打断是会有异常
try {
System.out.println("Before invoke: " + this.isInterrupted());//false
sleep(100);//sleep方法由于中断而抛出异常之后,线程的中断标志会被清除(置为false),所以这里打印都是false
} catch (InterruptedException e) {
e.printStackTrace();
//这里就可以优雅的方式结束线程生命周期
System.out.println("After invoke: " + this.isInterrupted());//false
break;
}
//第三种情况下:特殊情况下,如果在connection阻塞了,
// 没有机会读标记位,没机会去对线程的标记位进行判断,即方式1和2在这里就不行了
//就可以强制的关闭它,第17讲解
//readFile 在这里阻塞了,怎么处理???暴力的方法解决ThreadService对原生的Thread进行包装一下
}
//后面还有逻辑
//-------------
//-------------
//-------------
}
}
public static void main(String[] args) {
Worker worker = new Worker();
worker.start();
try {
//等待worker线程工作
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//规定的时间内没有完成就打断它
worker.interrupt();
}
}
1
public class ThreadCloseForce {
public static void main(String[] args) {
ThreadService service = new ThreadService();
long start = System.currentTimeMillis();
service.execute(() -> {
//集群之间拷贝文件,时间之内没完成就停止它,IO操作超时的巧妙结束线程的生命周期
//load a very heavy resource.
/*while (true) {
}*/
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//任务最多执行的时间
service.shutdown(5000);
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
2
public class ThreadService {//对一般的Thread进行了包装
private Thread executeThread;//执行线程
private volatile boolean finished = false;
//执行,让守护线程去做
public void execute(Runnable task) {
executeThread = new Thread() {
@Override
public void run() {
//里面设置一个守护线程干活
Thread runner = new Thread(task);
//执行线程退出了,守护线程也退出了,让守护线程执行,
runner.setDaemon(true);
//启动守护线程
runner.start();
//可能守护线程来不起启动去干活就死掉了,所以在下面要在executeThread上join,等待守护线程执行完
try {
//执行线程(executeThread)在这里阻塞住了
runner.join();
finished = true;
} catch (InterruptedException e) {
//e.printStackTrace();
}
}
};
executeThread.start();
}
//关闭,设置最多执行的时间
public void shutdown(long mills) {
long currentTime = System.currentTimeMillis();
while (!finished) {
if ((System.currentTimeMillis() - currentTime) >= mills) {
System.out.println("任务超时,需要结束他!");
executeThread.interrupt();//打断后执行线程(executeThread)就执行完了,它的生命周期结束了,开启的守护线程也就死了
break;
}
//既没有超时也没用执行结束,短暂的休眠一下
try {
executeThread.sleep(1);
} catch (InterruptedException e) {
System.out.println("执行线程被打断!");
break;
}
}
finished = false;
}
}