7

关闭线程池的正确姿势,shutdown(), shutdownNow()和awaitTermination() 该怎么用?

ExecutorService 接口提供了三个方法用于手动关闭线程池,分别是shutdown(),shutdownNow()awaitTermination()。我们最经常使用的 ThreadPoolExecutor 正是 ExecutorService 的实现类,自然也实现了这些方法。相信有很多小伙伴都没搞明白这些方法的区别,也不清楚在不同的情况下应该使用哪个方法。本文将通过ThreadPoolExecutor源码分析和简单用例向你展示这些方法的区别联系和使用场景,很多重点都在代码块的注释中,注意看哦!话不多说,上源码。

一、源码分析

1.1 线程池运行状态

ThreadPoolExecutor 使用 runState (运行状态)这个变量对线程池的生命周期进行控制,线程池关闭过程会有频繁的运行状态转化,所以我们首先需要了解线程池的各种运行状态及其之间的转化关系,runState 一共有以下5种取值:

  • RUNNING:接收新的任务并对任务队列里的任务进行处理;
  • SHUTDOWN:不再接收新的任务,但是会对任务队列中的任务进行处理;
  • STOP:不接收新任务,也不再对任务队列中的任务进行处理,并中断正在处理的任务;
  • TIDYING:所有任务都已终止,线程数为0,在转向TIDYING状态的过程中,线程会执行terminated()钩子方法,钩子方法是指在本类中是空方法,而在子类中进行具体实现的方法;
  • TERMINATED:terminated()方法执行结束后会进入这一状态,表示线程池已关闭。

线程池运行状态存储在AtomicInteger类型的变量ctl的最高三位中,因此各种状态所对应的整型变量的二进制格式除了最高三位,其余都是0 (如代码注释所示)。这些变量的操作会涉及到一些位运算和原子操作,现在只需要了解这些状态变量从小到大的顺序是RUNNING<SHUTDOWN<STOP<TIDYING<TERMINATED,另外很重要的一点就是这些状态的转化只能从小到大,不能从大到小。它们在源码中对应的变量如下:

// RUNNING: 十进制:-536870912  二进制:11100000000000000000000000000000
private static final int RUNNING    = -1 << COUNT_BITS; 
// SHUTDOWN: 十进制:0  二进制:0
private static final int SHUTDOWN   =  0 << COUNT_BITS;
// STOP: 十进制:536870912  二进制:00100000000000000000000000000000
private static final int STOP       =  1 << COUNT_BITS;
// TIDYING: 十进制:1073741824  二进制:01000000000000000000000000000000
private static final int TIDYING    =  2 << COUNT_BITS;
// TERMINATED: 十进制:1610612736  二进制:01100000000000000000000000000000
private static final int TERMINATED =  3 << COUNT_BITS;

// COUNT_BITS: 29
private static final int COUNT_BITS = Integer.SIZE - 3;

运行状态的转化条件和转化关系如下所示:

线程池运行状态转化关系图

线程池运行状态转化关系图

1.2 AtomicInteger 变量及相关操作

上面有提到过线程池的状态存储在一个 AtomicInteger 类型的变量 ctl 中,其实这个变量还存储了另一个线程池的重要信息,那就是线程数量 (workerCount)。从名字就可以看出,AtomicInteger 类对 int 类型进行了封装,可以对整数进行各种原子操作,适合在多线程环境中使用。大家都知道 int 类型有32位,而线程池的运行状态一共有5种,所以 ctl 的高三位足以表示所有的线程池运行状态 (23 = 8 > 5)。而另一个变量 workerCount 正好存储在其低29位中 (取值范围是0到229-1)。ThreadPoolExecutor 的源码中有很多对 ctl 变量的操作,在这里先把他们搞懂,后面就不会懵逼了。

// 使用一个AtomicInteger类型的变量ctl同时控制线程池运行状态和线程数量,初始运行状态
// 为RUNNING,线程数为0
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// CAPACITY: 十进制: 536870911 二进制: 00011111111111111111111111111111
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;


// 获取线程池当前状态,CAPACITY取反后高三位都是1,低29位都是0,再和c进行按位与运算,即可获得runState变量
private static int runStateOf(int c)     { return c & ~CAPACITY; }
// CAPACITY高三位都是0,低29位都是0,和c进行按位与运算即可获得workerCount变量
private static int workerCountOf(int c)  { return c & CAPACITY; }
// 初始化ctl变量,runState和workerCount进行按位或运算即可将这两个变量存储在一个变量中
private static int ctlOf(int rs, int wc) { return rs | wc; }

对于大神Doug Lea的这一波操作,我只能说

妙啊

另外你还需要了解一下判断当前线程池运行状态的相关操作:

// 判断线程池当前运行状态是否小于给定值
private static boolean runStateLessThan(int c, int s) {
    return c < s;
}
// 判断线程池当前运行状态是否大于等于给定值
private static boolean runStateAtLeast(int c, int s) {
    return c >= s;
}
// 判断线程池是否处于RUNNING状态
private static boolean isRunning(int c) {
    return c < SHUTDOWN;
}

好了,相信有了上面这些知识铺垫,下面的源码分析就难不倒你了。

1.3 shutdown() 及相关方法分析

下面会对shutdown()过程中涉及到的重要方法进行源码分析,注意看代码块的注释

1.3.1 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 {@link #awaitTermination awaitTermination} to do that.

开始有序关闭线程池,与此同时,已提交的任务将继续执行,但不再接收新的任务,如果线程池已关闭,此方法调用不会产生额外影响。此方法不会等待已提交的任务执行完毕,要等待已提交任务执行完毕可以使用 awaitTermination() 方法。

刚开始看这段注释的时候真是百思不得其解,前面说已提交的任务会继续执行,后面又说此方法不会等待已提交的任务执行完毕,这不是自相矛盾吗?后来通过自己写测试例子才终于搞懂,原来后半句说此方法不会等任务执行完毕的意思是这个方法不会阻塞,也就是说任务队列的任务会继续执行,但是其他的线程可能会抢在这些任务之前执行,而awaitTermination()方法可以实现阻塞并等待任务执行完毕,具体使用方法参考本文后面的例子。

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    // 上锁,确保同一时间只能有一个线程执行此操作
    mainLock.lock();
    try {
        // 检查方法调用方是否用权限关闭线程池以及中断工作线程
        checkShutdownAccess();
        // 将线程池运行状态设置为SHUTDOWN
        advanceRunState(SHUTDOWN);
        // 中断所有空闲线程
        interruptIdleWorkers();
        // 此方法在ThreadPoolExecutor中是空实现,具体实现在其子类ScheduledThreadPoolExecutor
        // 中,用于取消延时任务。
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
      // 尝试将线程池置为TERMINATED状态
    tryTerminate();
}

1.3.2 advanceRunState() 方法

通过 CAS 自旋操作 (也就是死循环)将线程池运行状态设置为目标值,如果已经大于等于目标值,则不作任何操作

private void advanceRunState(int targetState) {
    // 程序循环执行,直到将运行状态设为目标值
    for (;;) {
        // 获取AtomicInteger变量中的整数值
        int c = ctl.get();
        // if条件的前半部分是判断当前运行状态是否大于等于给定值
        if (runStateAtLeast(c, targetState) ||
            // 后半部分利用CAS操作将运行状态设置为目标值,成功的话会返回true,失败则返回false
            ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
            break;
     }
 }

1.3.3 InterruptIdleWorkers() 方法

中断所有的空闲线程

// 下面第二个方法的重载,将参数设为false,也就是中断所有空闲线程
private void interruptIdleWorkers() {
    interruptIdleWorkers(false);
}
// 中断空闲线程,如果参数为false,中断所有空闲线程,如果为true,则只中断一个空闲线程
private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            // 如果线程还未中断且处于空闲状态,将其中断。if条件的前半部分很好理解,后半部分 w.tryLock() 方法 
            // 调用了tryAcquire(int)方法 (继承自AQS),也就是尝试以独占的方式获取资源,成功则返回true,表示            
            // 线程处于空闲状态;失败会返回false,表示线程处于工作状态。
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
              // 如果为true,表示只中断一个空闲线程,并退出循环,这一情况只会用在tryTerminate()方法中
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

1.3.4 tryTerminate() 方法

满足下列条件之一时,尝试将线程池运行状态设置为 TERMINATED

  1. 线程池处于 SHUTDOWN 状态,线程数为0且任务队列为空;
  2. 线程池处于 STOP 状态且线程数为0。

如果线程池除了线程数不为0,其他条件都已经满足关闭的要求,则会尝试中断一个空闲线程,以确保关闭线程池的信号继续传递下去。在进行任何可能导致线程池关闭条件 (即上述任一条件) 成立的操作后都必须调用此方法,比如在减少线程数量或从任务队列中取出任务时。下面通过注释对这一方法进行详解

final void tryTerminate() {
      // 依然是CAS自旋操作
    for (;;) {
          // 获取ctl中的整型值
        int c = ctl.get();
          // 如果是以下几种情况之一的,对应第一个if判断的几种状况,直接return,不错任何操作
          // 1.线程池处于RUNNING状态
          // 2.线程池处于TIDYING状态或TERMINATED状态
          // 3.线程池处于SHUTDOWN状态但是任务队列不为空
        if (isRunning(c) ||
            runStateAtLeast(c, TIDYING) ||
            (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
            return;
        // 经过上一个if判断的过滤,当前线程池除了线程数不为0,其他条件都已经满足关闭的要求
        // 所以进行下一个if判断,如果线程数不为0,则中断一个空闲线程并返回
        if (workerCountOf(c) != 0) {
            interruptIdleWorkers(ONLY_ONE);
            return;
        }
        // 方法进行到这里说明线程池所有条件都已经满足关闭的要求,下面的操作就是将线程池状态
        // 置为TERMINATED
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // 将线程池状态置为TIDYING
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                try {
                    // 本类中是空方法,子类有具体实现,用于进行关闭线程池的前置操作
                    terminated();
                } finally {
                    // 将线程池置为TERMINATED状态,且线程数为0
                    ctl.set(ctlOf(TERMINATED, 0));
                    // 唤醒其他正在等待的线程
                    termination.signalAll();
                }
                return;
            }
        } finally {
            mainLock.unlock();
        }
        // else retry on failed CAS,失败的话会继续循环执行CAS操作
    }
}

shutdown()过程的主要方法至此就介绍完毕了,下面来看shutdownNow()

1.4 shutdownNow() 及相关方法分析

1.4.1 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. These tasks are drained (removed) from the task queue upon return from this method. This method does not wait for actively executing tasks to terminate. Use {@link #awaitTermination awaitTermination} to do that. There are no guarantees beyond best-effort attempts to stop processing actively executing tasks. This implementation cancels tasks via {@link Thread#interrupt}, so any task that fails to respond to interrupts may never terminate.

尝试终止所有正在执行的任务,并停止处理等待队列中的的任务,最后将所有未执行的任务列表的形式返回,此方法会将任务队列中的任务移除并以列表形式返回。此方法不会等待正在执行的任务执行完毕,要等待任务执行完毕可以使用awaitTermination()方法。此方法会尽最大努力终止正在执行的任务,除此之外不做其他保证,因为此方法底层实现是通过 Thread 类的interrupt()方法终止任务的,所以interrupt()未能终止的任务可能无法结束。

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();
    }
      // 尝试将线程池置为TERMINATED状态
    tryTerminate();
    return tasks;
}

1.4.2 InterruptWorkers() 方法

中断所有的线程,不管线程是否正在运行

private void interruptWorkers() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
          // 中断所有线程
        for (Worker w : workers)
            // 调用内部类Worker自身方法中断线程
            w.interruptIfStarted();
    } finally {
        mainLock.unlock();
    }
}

1.4.3 drainQueue() 方法

此方法将任务队列中剩余的任务转移到新的列表中并返回。一般情况下借助 BlockingQueuedrainTo()方法即可,但是某些类型的任务队列的poll()drainTo()方法在移除元素的过程中可能会出现失败的情况,比如 DelayQueue 以及其他一些队列。在这种情况下,就需要一个一个地转移元素了。源码如下:

private List<Runnable> drainQueue() {
    BlockingQueue<Runnable> q = workQueue;
    ArrayList<Runnable> taskList = new ArrayList<Runnable>();
      // 调用BlockingQueue的drainTo()方法转移元素
    q.drainTo(taskList);
    if (!q.isEmpty()) {
          // 一个一个地转移元素
        for (Runnable r : q.toArray(new Runnable[0])) {
            if (q.remove(r))
                taskList.add(r);
        }
    }
    return taskList;
}

对于shutdownNow()过程的源码分析到这里就结束了,接下来看awaitTermination()方法。

1.5 awaitTermination() 及相关方法分析

1.5.1 awaitTermination() 方法

public boolean awaitTermination(long timeout, TimeUnit unit)
    throws InterruptedException {
      // 将超时时间转化为纳秒单位
    long nanos = unit.toNanos(timeout);
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (;;) {
              // 如果线程池已经是TERMINATED状态,返回true
            if (runStateAtLeast(ctl.get(), TERMINATED))
                return true;
              // 超时但是线程池未关闭,返回false
            if (nanos <= 0)
                return false;
              // 实现阻塞效果
            nanos = termination.awaitNanos(nanos);
        }
    } finally {
        mainLock.unlock();
    }
}

awaitTermination()方法的源码比较简单,就是阻塞一定的时间。但是其中用到了一个termination变量,这个变量在之前的tryTerminate()方法中也出现过,下面来了解一下。

1.5.2 termination 是什么来历

private final ReentrantLock mainLock = new ReentrantLock();
private final Condition termination = mainLock.newCondition();

可以了解到termination是由 ReentrantLocknewCondition()方法获取的,具体初始化流程如下

termination变量初始化过程

termination变量初始化过程

由流程图可知,terminationAbstractQueuedSynchronizer 内部类 ConditionObject 的内部类实例,该类实现了 Condition 接口,也就拥有了await(),signal()等实现线程阻塞和唤醒的方法,这方面的具体内容就不在本文中展开了。

终于把shutdown(),shutdownNow()awaitTermination()这几个方法的原理介绍完了,建议你打开 IDE 自己再看看具体的源码,这样会理解得更加透彻。下面来看一下这几个方法的简单 demo,加深一下理解。

二、代码示例

本部分使用的代码示例非常简单,只是为了演示这几个方法的使用效果

2.1 shutdown() 方法的使用

public class ShutdownTest {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 5; i++) {
              // lambda表达式内只能使用final变量,所以这里使用相同效果的String类
            String str = i + "";
            service.execute(() -> {
                System.out.println(str);
            });
        }
        service.shutdown();
        System.out.println("线程池已关闭");
    }
}

运行结果

0
1
线程池已关闭
2
3
4
Process finished with exit code 0

从运行结果可以看出,在调用shutdown()方法后,已添加的任务会继续执行完毕,但是不会阻塞,导致 main 线程先打印出“线程池已关闭”。前面分析shutdown()方法时说过,调用该方法后线程池不再接收新的任务,那如果继续添加任务会怎样呢?把上述代码稍作修改

public static void main(String[] args) throws InterruptedException {
    ExecutorService service = Executors.newFixedThreadPool(2);
    for (int i = 0; i < 5; i++) {
        String str = i + "";
        service.execute(() -> {
            System.out.println(str);
        });
    }
    service.shutdown();
      // 调用shutdown()方法后继续添加任务
    service.execute(() -> System.out.println("ok"));
    System.out.println("线程池已关闭");
}

运行结果

0
1
3
2
4
Exception in thread "main" java.util.concurrent.RejectedExecutionException: ...
Process finished with exit code 1

可见如果在shutdown()方法调用后继续往线程池添加任务,会直接执行拒绝策略,而默认的拒绝策略是抛出异常。

2.2 shutdown() 和 awaitTermination() 组合使用

为保证执行顺序,一般会把shutdown() awaitTermination() 方法组合使用,效果如下:

public static void main(String[] args) throws InterruptedException {
    ExecutorService service = Executors.newFixedThreadPool(2);
    for (int i = 0; i < 5; i++) {
        String str = i + "";
        service.execute(() -> {
            System.out.println(str);
        });
    }
      // shutdown() 和 awaitTermination() 组合使用
    service.shutdown();
    service.awaitTermination(1, TimeUnit.MINUTES);
    System.out.println("线程池已关闭");
}

运行结果

0
1
2
4
3
线程池已关闭
Process finished with exit code 0

这样就保证了线程池和其他线程的之间的执行顺序。

2.3 shutdownNow() 方法的使用

public class ShutdownNowTest {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(2);
        for (int i = 1; i <= 5; i++) {
            String str = i + "";
            service.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(str);
                }
                  // 为了后面展示未执行的任务,这里重写toString()方法
                @Override
                public String toString() {
                    return "这是第" + str + "个任务";
                }
            });
        }
          // 没有来得及执行的任务会以列表的形式返回
        List<Runnable> runnables = service.shutdownNow();
        System.out.println("线程池已关闭");
        for (Runnable runnable : runnables) {
            // 打印一下未执行的任务
            System.out.println(runnable);
        }
    }
}

运行结果

1
2
线程池已关闭
这是第3个任务
这是第4个任务
这是第5个任务
Process finished with exit code 0

从运行结果可以看出,在调用shutdownNow()方法后,线程池会立即关闭,未执行的任务会以列表的形式返回。

三、结语

在看完本文的源码分析和代码示例后,相信你对手动关闭线程池的方法一定有了更深刻的理解。不过纸上得来终觉浅,绝知此事要躬行,亲自去读读源码你更能体会到大神的设计思路和对代码极致性能的追求,动手敲敲代码你更能深刻理解这些方法的执行逻辑。加油!


IamHYN
1.6k 声望1k 粉丝

Java开发工程师,立志于输出干货...