【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  小结

非常感谢这本书哈,对线程池的一些方法状态流转又加强了一下认识,膜拜。

posted @ 2024-07-19 08:56  酷酷-  阅读(1386)  评论(0)    收藏  举报