【Java 线程池】优雅的关闭线程池
1 前言
最近在看一些高并发的书籍 《Java高并发核心编程》,对线程池的关闭人家写的挺好,嘿嘿就拿来记录一下,方便梳理。说到关闭就涉及到线程池的状态以及流转,每种关闭方式对线程池带来的影响又是什么等,你还记得么,一起来回忆回忆吧。
2 线程池的状态
线程池总共存在5种状态,定义在ThreadPoolExecutor类中,具体代码如下:
package java.util.concurrent; //省略import public class ThreadPoolExecutor extends AbstractExecutorService { private static final int RUNNING = -1 << COUNT_BITS; private static final int SHUTDOWN = 0 << COUNT_BITS; private static final int STOP = 1 << COUNT_BITS; private static final int TIDYING = 2 << COUNT_BITS; private static final int TERMINATED = 3 << COUNT_BITS; }
线程池的5种状态具体如下:
1)RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。
2)SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕。
3)STOP:该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会 中断所有工作线程。
4)TIDYING:该状态下所有任务都已终止或者处理完成,将会执行terminated()钩子方法。
5)TERMINATED:执行完terminated()钩子方法之后的状态。
线程池的状态转换规则为:
1)线程池创建之后状态为RUNNING。
2)执行线程池的shutdown()实例方法,会使线程池状态从RUNNING转变为SHUTDOWN。
3)执行线程池的shutdownNow()实例方法,会使线程池状态从RUNNING转变为STOP。
4)当线程池处于SHUTDOWN状态,执行其shutdownNow()方法会将其状态转变为STOP。
5)等待线程池的所有工作线程停止,工作队列清空之后,线程池状态会从STOP转变为 TIDYING。
6)执行完terminated()钩子方法之后,线程池状态从TIDYING转变为TERMINATED。
线程池的状态之间的转换规则如图:

3 线程池的关闭
优雅地关闭线程池主要涉及的方法有3种:
1)shutdown:是JUC提供一个有序关闭线程池的方法,此方法会等待当前工作队列中的剩余 任务全部执行完成之后才会执行关闭,但是此方法被调用之后线程池的状态转变为SHUTDOWN, 线程池不会再接收新的任务。
2)shutdownNow:是JUC提供一个立即关闭线程池的方法,此方法会打断正在执行的工作线 程,并且会清空当前工作队列中的剩余任务,返回的是尚未执行的任务。
3)awaitTermination:等待线程池完成关闭。在调用线程池的shutdown()与shutdownNow()方法 时,当前线程会立即返回,不会一直等待直到线程池完成关闭。如果需要等到线程池关闭完成,可 以调用awaitTermination()方法。
3.1 shutdown() 方法原理
shutdown()方法的源码大致如下:
public void shutdown() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { // 检查权限 checkShutdownAccess(); // 设置线程池状态 advanceRunState(SHUTDOWN); // 中断空闲线程 interruptIdleWorkers(); // 钩子函数,主要用于清理一些资源 onShutdown(); // hook for ScheduledThreadPoolExecutor } finally { mainLock.unlock(); } tryTerminate(); }
shutdown()方法首先加锁,其次检查调用者是否具有执行线程池关闭的JavaSecurity权限。接 着shutdown()方法会将线程池状态变为SHUTDOWN,在这之后线程池不再接受提交的新任务。此 时如果还继续往线程池提交任务,将会使用线程池拒绝策略响应,默认的拒绝策略将会使用 ThreadPoolExecutor.AbortPolicy,接收新任务时会抛出RejectedExecutionException异常。
3.2 shutdownNow() 方法的原理
shutdownNow()方法的源码大致如下:
public List<Runnable> shutdownNow() { List<Runnable> tasks; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { // 检查状态 checkShutdownAccess(); // 将线程池状态变为 STOP advanceRunState(STOP); // 中断所有线程,包括工作线程以及空闲线程 interruptWorkers(); // 丢弃工作队列中剩余任务 tasks = drainQueue(); } finally { mainLock.unlock(); } tryTerminate(); return tasks; }
shutdownNow()方法将会把线程池状态设置为STOP,然后中断所有线程(包括工作线程以及 空闲线程),最后清空工作队列,取出工作队列中所有未完成的任务返回给调用者。与有序的 shutdown()方法相比,shutdownNow()方法比较粗暴,直接中断工作线程。不过这里需要注意的是, 中断线程并不代表线程立刻结束,只是通过工作线程的interrupt()实例方法设置了中断状态,这里 需要用户程序主动配合线程进行中断操作。
3.3 awaitTermination() 方法的使用
调用了线程池shutdown()与shutdownNow()方法之后,用户程序都不会主动等待线程池关闭完 成,如果需要等到线程池关闭完成,需要调用awaitTermination()进行主动等待。调用方法大致如下:
threadPool.shutdown(); try { //一直等待,直到线程池完成关闭 while (!threadPool.awaitTermination(60,TimeUnit.SECONDS)){ System.out.println("线程池任务还未执行结束"); } } catch (InterruptedException e) { e.printStackTrace(); }
如果线程池完成关闭,awaitTermination()方法将会返回true,否则当等待时间超过指定时间后 将会返回false。如果需要调用awaitTermination(),建议不是永久等待,而是设置一定重试次数。下 面的代码参考了阿里巴巴著名的分布式框架Dubbo中线程池关闭源码中的部分代码:
if (!threadPool.isTerminated()) { try { // 循环关闭1000次,每次等待10毫秒 for (int i = 0; i < 1000; i++) { if (threadPool.awaitTermination(10, TimeUnit.MILLISECONDS)) { break; } threadPool.shutdownNow(); } } catch (InterruptedException e) { System.err.println(e.getMessage()); } catch (Throwable e) { System.err.println(e.getMessage()); } }
4 优雅地关闭线程池
大家可以结合shutdown()、shutdownNow()和awaitTermination()三个方法去优雅关闭一个线程池, 大致分为以下几步:
1)执行shutdown()方法,拒绝新任务的提交,并等待所有任务有序地执行完毕。
2)执行awaitTermination(long timeout,TimeUnit unit)方法,指定超时时间,判断是否已经关 闭所有任务,线程池关闭完成。
3)如果awaitTermination()方法返回false,或者被中断,就调用shutDownNow()方法立即关闭 线程池所有任务。
4)补充执行awaitTermination(long timeout,TimeUnit unit)方法,判断线程池是否关闭完成。 如果超时,就可以进入循环关闭,循环一定的次数(如1000次),不断关闭线程池,直到其关闭或 者循环结束。
优雅地关闭线程池的参考代码具体如下:
import java.util.concurrent.ExecutorService; //省略import public class ThreadUtil { public static void shutdownThreadPoolGracefully(ExecutorService threadPool) { //若已经关闭则返回 if (!(threadPool instanceof ExecutorService) || threadPool.isTerminated()) { return; } try { threadPool.shutdown(); //拒绝接受新任务 } catch (SecurityException e) { return; } catch (NullPointerException e) { return; } try { // 等待60秒,等待线程池中的任务完成执行 if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) { // 调用shutdownNow()方法取消正在执行的任务 threadPool.shutdownNow(); // 再次等待60秒,如果还未结束,可以再次尝试,或者直接放弃 if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) { System.err.println("线程池任务未正常执行结束"); } } } catch (InterruptedException ie) { // 捕获异常,重新调用shutdownNow()方法 threadPool.shutdownNow(); } //仍然没有关闭,循环关闭1000次,每次等待10毫秒 if (!threadPool.isTerminated()) { try { for (int i = 0; i < 1000; i++) { if (threadPool.awaitTermination(10, TimeUnit.MILLISECONDS)) { break; } threadPool.shutdownNow(); } } catch (InterruptedException e) { System.err.println(e.getMessage()); } catch (Throwable e) { System.err.println(e.getMessage()); } } } //省略不相关代码 }
5 注册JVM钩子函数自动关闭线程池
如果使用了线程池,可以在JVM注册一个钩子函数,在JVM进程关闭之前,由钩子函数自动 将线程池优雅关闭,以确保资源正常释放。
下面的例子使用JVM钩子函数关闭了一个定义在随书源码的ThreadUtil辅助类中用于执行定 时、顺序任务的线程池,具体代码如下:
import java.util.concurrent.Callable; //省略import public class ThreadUtil { //懒汉式单例创建线程池:用于执行定时、顺序任务 static class SeqOrScheduledTargetThreadPoolLazyHolder { //线程池:用于定时任务、顺序排队执行任务 static final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(1, new CustomThreadFactory("seq")); static { //注册JVM关闭时的钩子函数 Runtime.getRuntime().addShutdownHook( new ShutdownHookThread("定时和顺序任务线程池", new Callable<Void>() { @Override public Void call() throws Exception { //优雅地关闭线程池 shutdownThreadPoolGracefully(EXECUTOR); return null; } })); } } //省略不相关代码 }
6 小结
非常感谢这本书哈,对线程池的一些方法状态流转又加强了一下认识,膜拜。

浙公网安备 33010602011771号