关闭线程池 shutdown 和 shutdownNow 的区别

VSole2021-12-18 16:26:04

前言

本章分为两个议题

如何正确关闭线程池

shutdown 和 shutdownNow 的区别

项目环境

jdk 1.8

github 地址:https://github.com/huajiexiewenfeng/java-concurrent

本章模块:threadpool

1.线程池示例

public class ShutDownThreadPoolDemo {

    private ExecutorService service = Executors.newFixedThreadPool(10);
    
    public static void main(String[] args) {
        new ShutDownThreadPoolDemo().executeTask();
    }
    
    public void executeTask() {
        for (int i = 0; i < 100; i++) {
            service.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "->执行");
            });
        }
    }

}

执行结果

pool-1-thread-2->执行
pool-1-thread-3->执行
pool-1-thread-1->执行
pool-1-thread-4->执行
pool-1-thread-5->执行
pool-1-thread-6->执行
...

执行完成之后,主线程会一直阻塞,那么如何关闭线程池呢?本章介绍 5 种在 ThreadPoolExecutor 中涉及关闭线程池的方法,如下所示

  • void shutdown
  • boolean isShutdown
  • boolean isTerminated
  • boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException
  • ListshutdownNow

2.shutdown

第一种方法叫作 shutdown(),它可以安全地关闭一个线程池,调用 shutdown() 方法之后线程池并不是立刻就被关闭,因为这时线程池中可能还有很多任务正在被执行,或是任务队列中有大量正在等待被执行的任务,调用 shutdown() 方法后线程池会在执行完正在执行的任务和队列中等待的任务后才彻底关闭。

调用 shutdown() 方法后如果还有新的任务被提交,线程池则会根据拒绝策略直接拒绝后续新提交的任务。

这段源码位置(jdk 1.8 版本)

java.util.concurrent.ThreadPoolExecutor#execute

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    // 线程池中的线程比核心线程数少 
    if (workerCountOf(c) < corePoolSize) {
        // 新建一个核心线程执行任务
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 核心线程已满,但是任务队列未满,添加到队列中
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 任务成功添加到队列以后,再次检查是否需要添加新的线程,因为已存在的线程可能被销毁了
        if (! isRunning(recheck) && remove(command))
            // 如果线程池处于非运行状态,并且把当前的任务从任务队列中移除成功,则拒绝该任务
            reject(command);
        else if (workerCountOf(recheck) == 0)
            // 如果之前的线程已经被销毁完,新建一个非核心线程
            addWorker(null, false);
    }
    // 核心线程池已满,队列已满,尝试创建一个非核心新的线程
    else if (!addWorker(command, false))
        // 如果创建新线程失败,说明线程池关闭或者线程池满了,拒绝任务
        reject(command);
}

1373 行 if (! isRunning(recheck) && remove(command)) 如果线程池被关闭,将当前的任务从任务队列中移除成功,并拒绝该任务

1378 行 else if (!addWorker(command, false)) 如果创建新线程失败,说明线程池关闭或者线程池满了,拒绝任务。

3.isShutdown

第二个方法叫作 isShutdown(),它可以返回 true 或者 false 来判断线程池是否已经开始了关闭工作,也就是是否执行了 shutdown 或者 shutdownNow 方法。

这里需要注意,如果调用 isShutdown() 方法的返回的结果为 true 并不代表线程池此时已经彻底关闭了,这仅仅代表线程池开始了关闭的流程,也就是说,此时可能线程池中依然有线程在执行任务,队列里也可能有等待被执行的任务。

4.isTerminated

第三种方法叫作 isTerminated(),这个方法可以检测线程池是否真正“终结”了,这不仅代表线程池已关闭,同时代表线程池中的所有任务都已经都执行完毕了。

比如我们上面提到的情况,如果此时已经调用了 shutdown 方法,但是还有任务没有执行完,那么此时调用 isShutdown 方法返回的是 true,而 isTerminated 方法则会返回 false。

直到所有任务都执行完毕了,调用 isTerminated() 方法才会返回 true,这表示线程池已关闭并且线程池内部是空的,所有剩余的任务都执行完毕了。

5.awaitTermination

第四个方法叫作 awaitTermination(),它本身并不是用来关闭线程池的,而是主要用来判断线程池状态的。

比如我们给 awaitTermination 方法传入的参数是 10 秒,那么它就会陷入 10 秒钟的等待,直到发生以下三种情况之一:

等待期间(包括进入等待状态之前)线程池已关闭并且所有已提交的任务(包括正在执行的和队列中等待的)都执行完毕,相当于线程池已经“终结”了,方法便会返回 true 等待超时时间到后,第一种线程池“终结”的情况始终未发生,方法返回 false 等待期间线程被中断,方法会抛出 InterruptedException 异常 调用 awaitTermination 方法后当前线程会尝试等待一段指定的时间,如果在等待时间内,线程池已关闭并且内部的任务都执行完毕了,也就是说线程池真正“终结”了,那么方法就返回 true,否则超时返回 fasle。

6.shutdownNow

最后一个方法是 shutdownNow(),它和 shutdown() 的区别就是多了一个 Now,表示立刻关闭的意思,不推荐使用这一种方式关闭线程池。

在执行 shutdownNow 方法之后,首先会给所有线程池中的线程发送 interrupt 中断信号,尝试中断这些任务的执行,然后会将任务队列中正在等待的所有任务转移到一个 List 中并返回,我们可以根据返回的任务 List 来进行一些补救的操作,例如记录在案并在后期重试。

shutdownNow 源码如下:

public List shutdownNow() {
    List tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(STOP);
        interruptWorkers();
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

interruptWorkers

让每一个已经启动的线程都中断,这样线程就可以在执行任务期间检测到中断信号并进行相应的处理,提前结束任务

7.shutdown 和 shutdownNow 的区别?

  • shutdown 会等待线程池中的任务执行完成之后关闭线程池,而 shutdownNow 会给所有线程发送中断信号,中断任务执行,然后关闭线程池
  • shutdown 没有返回值,而 shutdownNow 会返回关闭前任务队列中未执行的任务集合(List)
线程池线程阻塞
本作品采用《CC 协议》,转载必须注明作者和本文链接
多线程是Java的一个难点,但是它也很有趣,听说玩得溜得起飞的人,人生都开启多线程模式了…
前言 本章分为两个议题 如何正确关闭线程 shutdown 和 shutdownNow 的区别 项目环境 jdk 1.8 github 地址:https://github.com/huajiexiewenfeng/java-concurrent 本章模块:threadpool 1.线程示例
通过阅读本篇文章你将了解到: CompletableFuture的使用 CompletableFure异步和同步的性能测试 已经有了Future为什么仍需要在JDK1.8中引入CompletableFuture CompletableFuture的应用场景 对CompletableFuture的使用优化
同时例如 jstack、jmap 等工具也是不囿于一个方面的问题的,基本上出问题就是df、free、top 三连,然后依次jstack、jmap伺候,具体问题具体分析即可。CPU 异常往往还是比较好定位的。
先看一个小例子,明白如何创建一个 Caffeine 缓存实例。因为如果提前能预估缓存的使用大小,那么可以设置缓存的初始容量,以免缓存不断地进行扩容,致使效率不高。
任务模块专注于任务的执行等操作,开发和维护更加简单和高效;接收“调度中心”的执行请求、终止请求和日志请求等。XXL-JOB的不同任务之间并行调度、并行执行。同时支持任务终止。当任务”路由策略”选择”故障转移”时,当调度中心每次发起调度请求时,会按照顺序对执行器发出心跳检测请求,第一个检测为存活状态的执行器将会被选定并发送调度请求。
一、异步执行 实现方式二种: 使用异步注解@aysnc、启动类:添加@EnableAsync注解 JDK 8本身有一个非常好用的Future类——CompletableFuture
最常见的情况是使用 Tomcat 作为 Java Web 服务器,使用 Spring 提供的开箱即用的强大 的功能,并依赖其他开源库来完成负责的业务功能实现
如果是在消费端丢失数据,那么多次消费结果完全一模一样的几率很低。这时已经fetch的数据还没有处理完成但已经被commit掉,因此没有机会再次被处理,数据丢失。网络负载很高或者磁盘很忙写入失败的情况下,没有自动重试重发消息。
crawlergo是一个使用chrome headless模式进行URL收集的浏览器爬虫。它对整个网页的关键位置与DOM渲染阶段进行HOOK,自动进行表单填充并提交,配合智能的JS事件触发,尽可能的收集网站暴露出的入口。内置URL去重模块,过滤掉了大量伪静态URL,对于大型网站仍保持较快的解析与抓取速度,最后得到高质量的请求结果集合。调研1.
VSole
网络安全专家