前面咱们都是在讲如何建立线程
,接下来咱们说下如何终止线程
。java
java的线程小节中,我曾讲过:线程执行完或者出现异常就会进入终止状态。这样看,终止一个线程看上去很简单啊!一个线程执行完本身的任务,本身进入终止状态,这的确很简单。不过咱们今天谈到的“优雅地终止线程”,不是本身终止本身,而是在一个线程 T1 中,终止线程 T2;这里所谓的“优雅”,指的是给 T2 一个机会料理后事,而不是被直接终止。segmentfault
Java 语言的 Thread 类中曾经提供了一个 stop() 方法,用来终止线程,但是早已不建议使用了,缘由是这个方法用是直接终止的线程,线程并无机会料理后事。设计模式
前辈们通过认真对比分析,已经总结出了一套成熟的方案,叫作两阶段终止模式
。顾名思义,就是将终止过程分红两个阶段,其中第一个阶段主要是线程 T1 向线程 T2发送终止指令
,而第二阶段则是线程 T2响应终止指令
并发
两阶段终止模式示意图###性能
那在 Java 语言里,终止指令是什么呢?这个要从 Java 线程的状态转换过程提及。咱们在 java的线程小节中曾经提到过 Java 线程的状态转换图。线程
从这个图里你会发现,Java 线程进入终止状态的前提是线程进入 RUNNABLE 状态,而实际上线程也可能处在休眠状态,也就是说,咱们要想终止一个线程,首先要把线程的状态从休眠状态转换到 RUNNABLE 状态。如何作到呢?这个要靠 Java Thread 类提供的interrupt() 方法
,它能够将休眠状态的线程转换到 RUNNABLE 状态。设计
线程转换到 RUNNABLE 状态以后,咱们如何再将其终止呢?RUNNABLE 状态转换到终止状态,优雅的方式是让 Java 线程本身执行完 run() 方法,因此通常咱们采用的方法是设置一个标志位
,而后线程会在合适的时机检查这个标志位,若是发现符合终止条件,则自动退出 run() 方法。这个过程其实就是咱们前面提到的第二阶段:响应终止指令
代理
综合上面这两点,咱们能总结出终止指令,其实包括两方面内容:interrupt() 方法
和线程终止的标志位
。code
实际工做中,有些监控系统须要动态地采集一些数据,通常都是监控系统发送采集指令给被监控系统的监控代理,监控代理接收到指令以后,从监控目标收集数据,而后回传给监控系统,详细过程以下图所示。出于对性能的考虑(有些监控项对系统性能影响很大,因此不能一直持续监控),动态采集功能通常都会有终止操做。队列
动态采集功能示意图###
下面的示例代码是监控代理
简化以后的实现,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(); } }
线程池提供了两个方法:shutdown()和shutdownNow()
咱们曾经讲过,Java 线程池是生产者 - 消费者模式的一种实现,提交给线程池的任务,首先是进入一个阻塞队列中,以后线程池中的线程从阻塞队列中取出任务执行。
shutdown() 方法是一种很保守的关闭线程池的方法。线程池执行 shutdown() 后,就会拒绝接收新的任务,可是会等待线程池中正在执行的任务和已经进入阻塞队列的任务都执行完以后才最终关闭线程池。
而 shutdownNow() 方法,相对就激进一些了,线程池执行 shutdownNow() 后,会拒绝接收新的任务,同时还会中断线程池中正在执行的任务,已经进入阻塞队列的任务也被剥夺了执行的机会,不过这些被剥夺执行机会的任务会做为 shutdownNow() 方法的返回值返回。由于 shutdownNow() 方法会中断正在执行的线程,因此提交到线程池的任务,若是须要优雅地结束,就须要正确地处理线程中断。
两阶段终止模式是一种应用很普遍的并发设计模式,在 Java 语言中使用两阶段终止模式来优雅地终止线程,须要注意两个关键点:一个是仅检查终止标志位是不够的,由于线程的状态可能处于休眠态;另外一个是仅检查线程的中断状态也是不够的,由于咱们依赖的第三方类库极可能没有正确处理中断异常。
当你使用 Java 的线程池来管理线程的时候,须要依赖线程池提供的 shutdown() 和 shutdownNow() 方法来终止线程池。不过在使用时须要注意它们的应用场景,尤为是在使用 shutdownNow() 的时候,必定要谨慎。