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

对线程池 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 的正确关闭方法