对线程池 ExecutorService 的各种关闭方式的研究

本贴最后更新于 2828 天前,其中的信息可能已经时移世异

对线程池 ExecutorService 的关闭的研究

最近在使用 ExecutorService 的时候,对其关闭操作的概念非常模糊。查阅了许多文章、问答之后,有了一个总结。

1. 概述

Java 提供的接口 java.util.concurrent.ExecutorService 是一种异步执行的机制,可以让任务在后台执行。其实例就像一个线程池,可以对任务进行统一的管理。

2. 研究

1. 理论研究

Java 提供的对 ExecutorService 的关闭方式有两种,一种是调用其 shutdown() 方法,另一种是调用 shutdownNow() 方法。这两者是有区别的。

以下内容摘自源代码内的注释

// shutdown()
Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted.
Invocation has no additional effect if already shut down.
This method does not wait for previously submitted tasks to complete execution.  Use awaitTermination to do that.
// shutdownNow()
Attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution.
This method does not wait for actively executing tasks to terminate.  Use awaitTermination to do that.
There are no guarantees beyond best-effort attempts to stop processing actively executing tasks.  For example, typical implementations will cancel via interrupt, so any task that fails to respond to interrupts may never terminate.

两大段英文是什么意思呢?我来简单总结一下。
shutdown:

  1. 调用之后不允许继续往线程池内继续添加线程;
  2. 线程池的状态变为 SHUTDOWN 状态;
  3. 所有在调用 shutdown() 方法之前提交到 ExecutorSrvice 的任务都会执行;
  4. 一旦所有线程结束执行当前任务,ExecutorService 才会真正关闭。

shutdownNow():

  1. 该方法返回尚未执行的 task 的 List;
  2. 线程池的状态变为 STOP 状态;
  3. 阻止所有正在等待启动的任务,并且停止当前正在执行的任务;

简单点来说,就是:
shutdown() 调用后,不可以再 submit 新的 task,已经 submit 的将继续执行
shutdownNow() 调用后,试图停止当前正在执行的 task,并返回尚未执行的 task 的 list

2. 源码分析

针对源代码的分析,可以参考这篇文章
JAVA 线程池 shutdown 和 shutdownNow 的区别

3. 实战

所有实战代码,均可在 github 上下载并使用。github

1. Test1

// Test1
ExecutorService service = Executors.newFixedThreadPool(2);
Runnable run = () -> {
    try {
        Thread.sleep(5000);
        System.out.println("thread finish");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};
service.execute(run);
service.shutdown();
service.execute(run);

当调用 shutdown() 之后,将不能继续添加任务,否则会抛出异常 RejectedExecutionException。并且当正在执行的任务结束之后才会真正结束线程池。shutdownNow() 的试验可以看 Test2。

2. Test2

// Test2
ExecutorService service = Executors.newFixedThreadPool(2);
Runnable run = () -> {
    try{
        Thread.sleep(5000);
        System.out.println("thread finish");
    }catch(InterruptedExceptione){
        e.printStackTrace();
    }
};

service.execute(run);
service.shutdownNow();

使用 shutdownNow(),若线程中有执行 sleep/wait/定时锁 等,直接终止正在运行的线程并抛出 interrupt 异常。因为其内部是通过 Thread.interrupt() 实现的。
但是这种方法有很强的局限性。因为如果线程中没有执行 sleep 等方法的话,其无法终止线程。如接下来的 Test3 所示。

3. Test3

// Test3
ExecutorService service = Executors.newFixedThreadPool(1);
Runnable run = () -> {
    long num = 0;
    boolean flag = true;
    while(flag) {
        num += 1;
        if(num == Long.MAX_VALUE) flag = false;
    }
};
service.execute(run);
service.shutdownNow();

很多代码中都会有这样的情况,比方说使用循环标记 flag 循环执行一些耗时长的计算任务, 直到满足某个条件之后才设置循环标记为 false
如 Test3 代码所示(循环等待的情况),shutdownNow() 无法终止线程。
如果遇到这种情况,可以使用如 Test4 中的方法。

4. Test4

// Test4
ExecutorService service = Executors.newFixedThreadPool(1);
Runnable run = () -> {
    long sum = 0;
    while(true && !Thread.currentThread().isInterrupted()) {
        sum += 1;
    }
};
service.execute(run);
service.shutdownNow();

对于循环等待的情况,可以引入变量 Thread.currentThread().isInterrupted() 来作为其中的一个判断条件。
具体可参见 Stop an infinite loop in an ExecutorService task
isInterrupted() 方法返回当前线程是否有被 interrupt。
shutdownNow() 的内部实现实际上就是通过 interrupt 来终止线程,所以当调用 shutdownNow() 时,isInterrupted() 会返回 true
此时就可以跳出循环等待。
然而这也不是最优雅的解决方式,具体可以参见 Test5。

5. Test5

// Test5
ExecutorService service = Executors.newFixedThreadPool(1);
Runnable run = () -> {
    long sum = 0;
    boolean flag = true;
    while(flag && !Thread.currentThread().isInterrupted()) {
        sum += 1;
        if(sum == Long.MAX_VALUE) flag = false;
    }
};
service.execute(run);
service.shutdown();
try {       
    if(!service.awaitTermination(2, TimeUnit.SECONDS)) {
        service.shutdownNow();
    }
} catch (InterruptedException e) {
    service.shutdownNow();
}

这里。先调用 shutdown() 使线程池状态改变为 SHUTDOWN,线程池不允许继续添加线程,并且等待正在执行的线程返回。
调用 awaitTermination 设置定时任务,代码内的意思为 2s 后检测线程池内的线程是否均执行完毕(就像老师告诉学生,“最后给你 2s 钟时间把作业写完”),若没有执行完毕,则调用 shutdownNow() 方法。

4. 关于更多

关于 shutdown(), shutdownNow()awaitTermination() 方法,我在网上发现一个非常优雅的举例。是一位日本人写的文章

我搬运一下译文。
翻译[Java]ExecutorService 的正确关闭方法

3. 参考资料

  1. Stop an infinite loop in an ExecutorService task
  2. ExecutorService 对象的 shutdown()和 shutdownNow()的区别
  3. shutdown 和 shutdownNow--多线程任务的关闭(转)
  4. 线程服务 ExecutorService 的操作 shutdown 方法和 shutdownNow 方法
  5. ExecutorService 的理解与使用
  6. 翻译[Java]ExecutorService 的正确关闭方法
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3187 引用 • 8213 回帖
  • 线程
    122 引用 • 111 回帖 • 3 关注
  • 研究
    12 引用 • 34 回帖
  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1063 引用 • 3453 回帖 • 203 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • zjhch123
    作者

    这同步怎么又不给力了= =

  • eddy

    666666666

  • jgsdlnr

    页面可爱

  • jgsdlnr

    一开始能点红心的 为什么又点不了了