一文教你安全的关闭线程池

上篇文章 ShutdownHook- Java 优雅停机解决方案 提到应用停机时需要释放资源,关闭连接。对于一些定时任务或者网络请求服务将会使用线程池,当应用停机时需要正确安全的关闭线程池,如果处理不当,可能造成数据丢失,业务请求结果不正确等问题。

关闭线程池我们可以选择什么都不做,JVM 关闭时自然的会清除线程池对象。当然这么做,存在很大的弊端,线程池中正在执行执行的线程以及队列中还未执行任务将会变得极不可控。所以我们需要想办法控制到这些未执行的任务以及正在执行的线程。

线程池 API 提供两个主动关闭的方法 ThreadPoolExecutor#shutdownNowThreadPoolExecutor#shutdown,这两个方法都可以用于关闭线程池,但是具体效果却不太一样。

线程池的状态

在说线程池关闭方法之前,我们先了解线程池状态。

线程池状态关系图如下:

从上图我们看到线程池总共存在 5 种状态,分别为:

  • RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。
  • SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行结束。
  • STOP: 该状态下线程池不再接受新任务,但是不会处理工作队列中的任务,并且将会中断线程。
  • TIDYING:该状态下所有任务都已终止,将会执行 terminated() 钩子方法。
  • TERMINATED:执行完 terminated() 钩子方法之后。

当我们执行 ThreadPoolExecutor#shutdown 方法将会使线程池状态从 RUNNING 转变为 SHUTDOWN。而调用 ThreadPoolExecutor#shutdownNow 之后线程池状态将会从 RUNNING 转变为 STOP。从上面的图上还可以看到,当线程池处于 SHUTDOWN,我们还是可以继续调用 ThreadPoolExecutor#shutdownNow 方法,将其状态转变为 STOP

ThreadPoolExecutor#shutdown

上面我们知道线程池状态,这里先说说 shutdown 方法。shutdown 方法源码比较简单,能比较直观理解其调用逻辑。

shutdown 方法源码:

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
       // 检查权限
            checkShutdownAccess();
            // 设置线程池状态
        advanceRunState(SHUTDOWN);
       // 中断空闲线程
            interruptIdleWorkers();
            // 钩子函数,主要用于清理一些资源
            onShutdown();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }

shutdown 方法首先加锁,其次先检查系统安装状态。接着就会将线程池状态变为 SHUTDOWN,在这之后线程池不再接受提交的新任务。此时如果还继续往线程池提交任务,将会使用线程池拒绝策略响应,默认情况下将会使用 ThreadPoolExecutor.AbortPolicy,抛出 RejectedExecutionException 异常。

interruptIdleWorkers 方法只会中断空闲的线程,不会中断正在执行任务的的线程。空闲的线程将会阻塞在线程池的阻塞队列上。

线程池构造参数需要指定 coreSize(核心线程池数量),maximumPoolSize(最大的线程池数量),keepAliveTime(多余空闲线程等待时间),unit(时间单位),workQueue(阻塞队列)。

当调用线程池的 execute 方法,线程池工作流程如下:

  1. 如果此时线程池中线程数量小于 coreSize,将会新建线程执行提交的任务。
  2. 如果此时线程池线程数量已经大于 coreSize,将会直接把任务加入到队列中。线程将会从工作队列中获取任务执行。
  3. 如果工作队列已满,将会继续新建线程。
  4. 如果工作队列已满,且线程数等于 maximumPoolSize,此时将会使用拒绝策略拒绝任务。
  5. 超过 coreSize 数量那部分线程,如果空闲了 keepAliveTime ,线程将会终止。

工作流程图如下:

当线程池处于第二步时,线程将会使用 workQueue#take 获取队头的任务,然后完成任务。如果工作队列一直没任务,由于队列为阻塞队列,workQueue#take 将会阻塞线程。

ThreadPoolExecutor#shutdownNow

ThreadPoolExecutor#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 方法比较粗暴,直接中断工作线程。不过这里需要注意,中断线程并不代表线程立刻结束。这里需要线程主动配合线程中断响应。

线程中断机制:
thread#interrupt 只是设置一个中断标志,不会立即中断正常的线程。如果想让中断立即生效,必须在线程 内调用 Thread.interrupted() 判断线程的中断状态。
对于阻塞的线程,调用中断时,线程将会立刻退出阻塞状态并抛出 InterruptedException 异常。所以对于阻塞线程需要正确处理 InterruptedException 异常。

awaitTermination

线程池 shutdownshutdownNow 方法都不会主动等待执行任务的结束,如果需要等到线程池任务执行结束,需要调用 awaitTermination 主动等待任务调用结束。

调用方法如下:

        threadPool.shutdown();
        try {
            while (!threadPool.awaitTermination(60,TimeUnit.SECONDS)){
                System.out.println("线程池任务还未执行结束");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

如果线程池任务执行结束,awaitTermination 方法将会返回 true,否则当等待时间超过指定时间后将会返回 false

如果需要使用这种进制,建议在上面的基础上增加一定重试次数。这个真的很重要!!!

优雅关闭线程池

回顾上面线程池状态关系图,我们可以知道处于 SHUTDOWN 的状态下的线程池依旧可以调用 shutdownNow。所以我们可以结合 shutdownshutdownNowawaitTermination ,更加优雅关闭线程池。

        threadPool.shutdown(); // Disable new tasks from being submitted
        // 设定最大重试次数
        try {
            // 等待 60 s
            if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
                // 调用 shutdownNow 取消正在执行的任务
                threadPool.shutdownNow();
                // 再次等待 60 s,如果还未结束,可以再次尝试,或则直接放弃
                if (!threadPool.awaitTermination(60, TimeUnit.SECONDS))
                    System.err.println("线程池任务未正常执行结束");
            }
        } catch (InterruptedException ie) {
            // 重新调用 shutdownNow
            threadPool.shutdownNow();
        }

文章首发于 studyidea.cn/close..

欢迎关注我的公众号:程序通事,获得日常干货推送。如果您对我的专题内容感兴趣,也可以关注我的博客:studyidea.cn

原文地址:https://www.cnblogs.com/goodAndyxublog/p/11664742.html

时间: 2024-10-29 01:57:33

一文教你安全的关闭线程池的相关文章

linux环境中关闭tomcat,通过shutdown.sh无法彻底关闭--线程池

最近测试环境上测试的项目通过shutdown.sh始终无法彻底关闭. 之前临时解决方法两种: 第一:通过ps -ef|grep tomcat查看到tomcat的进程直接使用kill来杀死进程. 第二: 基本原理为启动tomcat时记录启动tomcat的进程id(pid),关闭时强制杀死该进程 1.找到tomcat下bin/catalina.sh文件,vi进去添加点东西,主要是记录tomcat的pid,如下: 大概在第125行左右,添加如下代码 #设置CATALINA_PID(后加)if [ -z

dubbo如何关闭一个线程池的?

public static void gracefulShutdown(Executor executor, int timeout) { if (!(executor instanceof ExecutorService) || isShutdown(executor)) { return; } final ExecutorService es = (ExecutorService) executor; try { es.shutdown(); // Disable new tasks fro

如果优雅地关闭ExecutorService提供的java线程池

ExecutorService让我们可以优雅地在程序中使用线程池来创建和管理线程,而且性能佳.开销小,还可以有效地控制最大并发线程数,是我们在java并发编程中会经常使用到的. 每一个线程都会占用系统资源,因此线程池的关闭与清理同样重要,本文介绍我们如何优雅地关闭线程池. 一. ExecutorService中关闭线程池的方法 1. shutdown() 停止接收新任务,原来的任务继续执行 停止接收新的submit的任务: 已经提交的任务(包括正在跑的和队列中等待的),会继续执行完成: 等到第2

如何优雅的关闭Java线程池

面试中经常会问到,创建一个线程池需要哪些参数啊,线程池的工作原理啊,却很少会问到线程池如何安全关闭的. 也正是因为大家不是很关注这块,即便是工作三四年的人,也会有因为线程池关闭不合理,导致应用无法正常stop的情况,还有出现一些报错的问题. 本篇就以ThreadPoolExecutor为例,来介绍下如何优雅的关闭线程池. 01 线程中断 在介绍线程池关闭之前,先介绍下Thread的interrupt. 在程序中,我们是不能随便中断一个线程的,因为这是极其不安全的操作,我们无法知道这个线程正运行在

Java四种线程池newCachedThreadPool,newFixedThreadPool,newScheduledThreadPool,newSingleThreadExecutor

介绍new Thread的弊端及Java四种线程池的使用,对Android同样适用.本文是基础篇,后面会分享下线程池一些高级功能. 1.new Thread的弊端 执行一个异步任务你还只是如下new Thread吗? Java new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub } }).start(); 1 2 3 4 5 6 7 new Thread(new

Java底层技术系列文章-线程池框架

一.线程池结构图    二.示例 定义线程接口 public class MyThread extends Thread { @Override publicvoid run() { System.out.println(Thread.currentThread().getName() + "正在执行"); }}   1:newSingleThreadExecutor ExecutorService pool = Executors. newSingleThreadExecutor()

多线程篇六:线程池

1.固定大小的线程池 ExecutorService threadPools1=Executors.newFixedThreadPool(3); for(int i=1;i<=10;i++){ final int task=i; //循环10次,一共往线程池里面放10个任务 threadPools1.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().ge

java io学习 线程池

Java提供的原生线程池技术处理原理很清晰,故只要使用自己的原生线程池技术一般都能满足项目的需求.java提供了很好的线程池实现,比我们自己的实现要更加健壮以及高效,同时功能也更加强大,不建议自己编写.另外有同学可能用过spring的线程池,那么spring线程池和jdk原生线程池有啥区别吗?我们查看源码和官方api可以知道SpringFrameWork 的 ThreadPoolTaskExecutor 是辅助 JDK 的 ThreadPoolExecutor 的工具类,它将属性通过 JavaB

Java四种线程池

Java四种线程池newCachedThreadPool,newFixedThreadPool,newScheduledThreadPool,newSingleThreadExecutor 时间:2015-10-20 22:37:40      阅读:8762      评论:0      收藏:0      [点我收藏+] 介绍new Thread的弊端及Java四种线程池的使用,对Android同样适用.本文是基础篇,后面会分享下线程池一些高级功能. 1.new Thread的弊端执行一个异