如何优雅关闭线程池

如何优雅关闭线程池

许多业务场景中都会到线程池,比如微服务接收外部调用、异步发送邮件、离线统计报表等等。线程池有三个优点:降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗;提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行;提高线程的可管理性,对线程进行统一的分配,调优和监控。

老话说,请神容易送神难,使用了线程池就要承受它带来的问题。线程池的运行机制比较复杂,使用线程池的参数并不好配置。线程池执行的情况与任务类型相关,IO密集型和CPU密集型的任务运行起来的情况差异很大,配置合理依赖开发人员的经验和知识。如果配置不当,反而降低系统效率。另外,还一个问题,如何优雅关闭线程池,避免任务或者数据丢失呢?

首先要搞清楚销毁线程池的基本方法。Java线程池ThreadPoolExecutor提供了三个重要的方法shutdown()、shutdownNow()和awaitTermination():

  • shutdown:拒绝新任务提交到线程池,待执行与正在执行的任务继续执行。
  • shutdownNow:拒绝提交新任务到线程池,取消队列中等待执行的任务,尝试取消正在执行任务。
  • awaitTermination(long timeOut, TimeUnit unit):阻塞线程,等待所有待执行以及正在执行的任务执行完成,或者等到超时,或者线程被中断抛出中断异常,然后返回true或false(表示已超时)

这三个方法的区别是:

  • shutdown与shutdownNow之后都拒绝提交任务,awaitTermination可以继续提交任务。
  • awaitTermination是阻塞的,返回结果是线程池是否已停止(true/false)shutdown/Now不阻塞。
  • shutdownNow是立即关闭,而shutdown是优雅关闭。

确定优雅关闭线程的步骤:
(1)执行shutdown方法,等待所有任务执行完毕并拒绝新任务的提交。
(2)执行awaitTermination(long timeout,TimeUnit unit),指定超时时间,判断是是否已经关闭所有任务,防止线程永远无法关闭。
(3)如果第2步返回fasle,或者被中断。调用shutDownNow方法立即关闭线程池所有任务。

根据这个步骤定义通用关闭线程池工具类:

//这个工具类来自dubbo源码
public class ExecutorUtil {
    private static final Logger logger = LoggerFactory.getLogger(ExecutorUtil.class);
    private static final ThreadPoolExecutor shutdownExecutor = new ThreadPoolExecutor(0, 1,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>(100),
            new NamedThreadFactory("Close-ExecutorService-Timer", true));

    public static boolean isTerminated(Executor executor) {
        if (executor instanceof ExecutorService) {
            if (((ExecutorService) executor).isTerminated()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Use the shutdown pattern from:
     *  https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html
     * @param executor the Executor to shutdown
     * @param timeout the timeout in milliseconds before termination
     */
    public static void gracefulShutdown(Executor executor, int timeout) {
        if (!(executor instanceof ExecutorService) || isTerminated(executor)) {
            return;
        }
        final ExecutorService es = (ExecutorService) executor;
        try {
            // Disable new tasks from being submitted
            es.shutdown();
        } catch (SecurityException ex2) {
            return;
        } catch (NullPointerException ex2) {
            return;
        }
        try {
            // Wait a while for existing tasks to terminate
            if (!es.awaitTermination(timeout, TimeUnit.MILLISECONDS)) {
                es.shutdownNow();
            }
        } catch (InterruptedException ex) {
            es.shutdownNow();
            Thread.currentThread().interrupt();
        }
        if (!isTerminated(es)) {
            newThreadToCloseExecutor(es);
        }
    }

    public static void shutdownNow(Executor executor, final int timeout) {
        if (!(executor instanceof ExecutorService) || isTerminated(executor)) {
            return;
        }
        final ExecutorService es = (ExecutorService) executor;
        try {
            es.shutdownNow();
        } catch (SecurityException ex2) {
            return;
        } catch (NullPointerException ex2) {
            return;
        }
        try {
            es.awaitTermination(timeout, TimeUnit.MILLISECONDS);
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
        }
        if (!isTerminated(es)) {
            newThreadToCloseExecutor(es);
        }
    }
}

1 在ServletContextListener接口中销毁

以Sping Boot工程为例,ServletContextListener接口是Servlet包中的接口,在Tomcat启动时会执行该接口对象的contextInitialized(ServletContextEvent sce)方法,当Tomcat关闭时会调用该接口对象的contextDestroyed(ServletContextEvent sce)方法。启动类中加上@ServletComponentScan注解

@SpringBootApplication
@ServletComponentScan
public class ShutdownThreadPoolApplication {

    public static void main(String[] args) {
        SpringApplication.run(ShutdownThreadPoolApplication.class, args);
    }

}
@WebListener
public class ApplicationWatch implements ServletContextListener {
    private static Logger logger = Logger.getLogger(ArchDataInit.class);
    public void contextDestroyed(ServletContextEvent event) {
    logger.info("start ApplicationWatch contextDestroyed method");
    logger.info("start shutdown thread pool");
    //传入外部定义的线程池executor
    ExecutorUtil.gracefulShutdown(executor, 20);  
    logger.info("end shutdown thread pool");
}

2 在DisposableBean接口中销毁

在Spring销毁bean时候,如果实现了DisposableBean接口会自动回调destroy方法,在该方法中再销毁线程池。


public class DestroyThreadPoolWhenShutApp implements DisposableBean{
    private static Logger logger = Logger.getLogger(DestroyThreadPoolWhenShutApp.class);
    public void destroy() throws Exception {
        logger.info("start ApplicationWatch contextDestroyed method");
        logger.info("start shutdown thread pool");
        //传入外部定义的线程池executor
        ExecutorUtil.gracefulShutdown(executor, 20);
        logger.info("end shutdown thread pool");
    }
}

Spring配置文件中创建一个bean级别的销毁bean

 <bean id="destroyThreadPool" class="com.demo.spring.DestroyThreadPoolWhenShutApp"/>

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注