【多线程】Java ThreadPoolExecutor的shutdown方法踩坑和源码阅读

概述

  最近刚好在学多线程的东西,并且项目中有用到多线程,然后在一次review代码的时候,大佬提出了一个问题,pool.shutdown()之后,不一定所有线程就立刻停止,最好加上 pool.awaitTermination()方法给一定的等待时间,给点时间让剩余线程结束。
  当时对这个东西不怎么熟悉,为什么 shutdown()后还是不会立刻把全部线程停止呢?然后去看了下源码的解析才发现,原来jdk是搞了两套逻辑。

线程池状态

  在看之前,先了解一下线程池的几个状态
线程池状态其实已经在 ThreadPoolExecutor中解析的了

/**
     * The main pool control state, ctl, is an atomic integer packing
     * two conceptual fields
     *   workerCount, indicating the effective number of threads
     *   runState,    indicating whether running, shutting down etc
     *
     * In order to pack them into one int, we limit workerCount to
     * (2^29)-1 (about 500 million) threads rather than (2^31)-1 (2
     * billion) otherwise representable. If this is ever an issue in
     * the future, the variable can be changed to be an AtomicLong,
     * and the shift/mask constants below adjusted. But until the need
     * arises, this code is a bit faster and simpler using an int.
     *
     * The workerCount is the number of workers that have been
     * permitted to start and not permitted to stop.  The value may be
     * transiently different from the actual number of live threads,
     * for example when a ThreadFactory fails to create a thread when
     * asked, and when exiting threads are still performing
     * bookkeeping before terminating. The user-visible pool size is
     * reported as the current size of the workers set.
     *
     * The runState provides the main lifecycle control, taking on values:
     *
     *   RUNNING:  Accept new tasks and process queued tasks
     *   SHUTDOWN: Don't accept new tasks, but process queued tasks
     *   STOP:     Don't accept new tasks, don't process queued tasks,
     *             and interrupt in-progress tasks
     *   TIDYING:  All tasks have terminated, workerCount is zero,
     *             the thread transitioning to state TIDYING
     *             will run the terminated() hook method
     *   TERMINATED: terminated() has completed
     *
     * The numerical order among these values matters, to allow
     * ordered comparisons. The runState monotonically increases over
     * time, but need not hit each state. The transitions are:
     *
     * RUNNING -> SHUTDOWN
     *    On invocation of shutdown(), perhaps implicitly in finalize()
     * (RUNNING or SHUTDOWN) -> STOP
     *    On invocation of shutdownNow()
     * SHUTDOWN -> TIDYING
     *    When both queue and pool are empty
     * STOP -> TIDYING
     *    When pool is empty
     * TIDYING -> TERMINATED
     *    When the terminated() hook method has completed
     *
     * Threads waiting in awaitTermination() will return when the
     * state reaches TERMINATED.
     *
     * Detecting the transition from SHUTDOWN to TIDYING is less
     * straightforward than you'd like because the queue may become
     * empty after non-empty and vice versa during SHUTDOWN state, but
     * we can only terminate if, after seeing that it is empty, we see
     * that workerCount is 0 (which sometimes entails a recheck -- see
     * below).
     */
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

这里面其实解析得很清楚,使用了一个cas的原子整数的头三个位数来表示线程状态 (所以此时最大的线程数肯定是(2^29)-1) 并且只用一个Int表示状态和数量的话,会比用两个int来表示会快(因为两个int表示就需要两次cas,而一个Int只需要一个或运算即可 具体看ctlOf()方法)
各种状态如下表
在这里插入图片描述

场景

了解了池的状态之后,我们开始从一个场景去慢慢熟悉 shutdown方法会有哪些坑
首先先上一段代码

public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        for (int i = 0; i < 3; i++) {
            int count = i;
            Future<?> future = pool.submit(() -> {
                try {
                    log.info("begin count: {}", count);
                    Thread.sleep(1000);
                    log.info("end count: {}", count);
                } catch (InterruptedException e) {
                    log.info("强制打断, count: {}", count);
                }
            });
        }
        
        Thread.sleep(1500);
        pool.shutdown();
        log.info("shutdown");
    }

打印内容为
在这里插入图片描述
所以其实我们可以看到,执行了shutdown之后,如果线程3还在执行的途中或还在队列中时,此时是不会因为shutdown而中断线程3的。
然后再看下一段代码

public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        CountDownLatch latch = new CountDownLatch(3);
        for (int i = 1; i < 4; i++) {
            int count = i;
            Future<?> future = pool.submit(() -> {
                try {
                    log.info("begin count: {}", count);
                    Thread.sleep(1000);
                    log.info("end count: {}", count);
                } catch (InterruptedException e) {
                    log.info("强制打断, count: {}", count);
                }
                latch.countDown();
            });
        }
        latch.await();

        Thread.sleep(1500);
        pool.shutdown();
        log.info("shutdown");
    }
 

打印内容为
在这里插入图片描述

具体原因

至于为什么会这样,我们主要根据之前说的线程状态,以及去看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.
     *
     * <p>This method does not wait for previously submitted tasks to
     * complete execution.  Use {@link #awaitTermination awaitTermination}
     * to do that.
     *
     * @throws SecurityException {@inheritDoc}
     */
    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            // 设置线程状态为shutdown
            advanceRunState(SHUTDOWN);
            // 中止空闲线程
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        // 尝试去终结线程池,方法内部会用ctl去进行判断来是否终结线程池,若未执行完毕则不终结
        tryTerminate();
    }

其实方法内部已经解析得非常清楚,调用shutdown()方法的话,将已经提交的任务继续执行,不接收新任务。所以若是直接调用shutdown方法的话,其实是有点风险的,比如你想在线程执行完后再执行主线程的方法, 若主线程直接shutdown的话,不一定能控制住整体的线程

使用建议

那我们应该怎么去使用shutdown方法呢,对于内部我列了如下建议,当然这只是非常小的一部分,还有很多使用方法建议都直接去看看源码使用即可,有非常详细的介绍及说明。在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值