深入理解ThreadPoolExecutor(下):线程池的关闭

系列文章目录


前言

上一篇文章讲了线程池的创建和启动,这一篇讲线程池的关闭。
可能刚开始大家会觉得线程池的关闭很容易理解,不就是调用shutdown()或shutdownNow()方法,中断线程,不再接受任务,等任务执行完,工作线程数量为0时,线程池就关闭了吗。但是,仔细想想,还是会有些疑惑:
1)中断线程时是怎么区分空闲线程和非空闲线程的呢?
2)中断空闲线程是怎么中断的?
3)中断非空闲线程是怎么中断的?如果一个线程正在执行任务,此时中断线程是真的将任务停止了吗?
带着疑惑,我们来深入理解线程池的关闭

一、线程池的5种状态

我们先回顾一下线程池的5种状态
在这里插入图片描述

1)RUNNING:能够接收新任务,以及对已添加的任务进行处理
2)SHUTDOWN:执行shutdown()方法会变成SHUTDOWN状态,不接收新任务,但能处理已添加的任务
3)STOP:不接收新任务,不处理已添加的任务,并且会中断正在处理的任务,执行shutdownNow()方法会变为STOP状态
4)TIDYING:所有的任务已终止,ctl记录的线程数量为0。会执行钩子函数terminated()
5)TERMINATED:执行完terminated()之后,就会由 TIDYING变为TERMINATED

二、shutdown()和shutdownNow()源码分析

1.shutdown()方法

public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(SHUTDOWN);
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }

主要做了这几件事:
1)advanceRunState(SHUTDOWN):自旋+CAS将线程池的状态修改为SHUTDOWN;
2)interruptIdleWorkers():中断空闲线程
3)onShutdown():钩子函数,留给子类实现
4)tryTerminate():向TERMINATED状态转变

所以我们要具体看interruptIdleWorkers()和tryTerminate()方法

interruptIdleWorkers()方法

private void interruptIdleWorkers() {
        interruptIdleWorkers(false);
    }

private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
        //遍历所有工作线程
            for (Worker w : workers) {
                Thread t = w.thread;
                //如果没有被中断,并且在工作状态
                if (!t.isInterrupted() && w.tryLock()) {
                    try {
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
    }

遍历所有工作线程,如果没有被中断,并且在工作状态就对该线程中断,所以我们看一下w.tryLock()方法

public boolean tryLock()  { return tryAcquire(1); }

上一篇文章讲过,worker实现了AQS的不可重入锁,这里实际上就是获取不可重入锁,获取到了就表示当前线程空闲,获取不到就表示当前线程正在工作

这里为啥用不可重入锁来表示空闲和工作状态,我的理解,首先肯定需要一个state变量来表示空闲和工作状态,所以这里就用了CAS的state,然后,为了避免在执行任务的过程中调用了shutdown方法而不会中断自己(因为自己本身正在执行任务),比如下面代码,如果是可重入锁,那么执行shutdown方法时,自己就把自己中断了(个人理解,可能不对,欢迎指正)

public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 4; i++) {
            Runnable task = () -> {
                try {
                    // 模拟任务执行
                    Thread.sleep(2000);
                    //模拟任务中调用了shutdown方法
                    executorService.shutdown();
                    Thread.sleep(2000);
                } catch (Exception e) {
                    // 处理中断异常
                    System.out.println(Thread.currentThread().getName()+"任务被中断。");
                    return;
                }
                System.out.println(Thread.currentThread().getName()+"任务结束。");
            };
            executorService.execute(task);
        }
    }

ps: 其实没必要像我一样深究这些,越底层的框架越需要严谨,所以实现会变得复杂,过于深究太浪费时间,我们只需要知道这里使用不可重入锁来表示空闲和工作状态即可

tryTerminate()方法

final void tryTerminate() {
        for (;;) {
            int c = ctl.get();
            if (isRunning(c) ||
                runStateAtLeast(c, TIDYING) ||
                (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
                return;
            if (workerCountOf(c) != 0) { // Eligible to terminate
                interruptIdleWorkers(ONLY_ONE);
                return;
            }

            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                    try {
                        terminated();
                    } finally {
                        ctl.set(ctlOf(TERMINATED, 0));
                        termination.signalAll();
                    }
                    return;
                }
            } finally {
                mainLock.unlock();
            }
            // else retry on failed CAS
        }
    }

1)自旋,如果是以下两种情况,可以去关闭线程池,其他情况(比如正在运行,比如工作线程数不为0,比如已经关闭)直接返回
a.线程池状态为SHUTDOWN并且工作线程数为0,等待队列为空
b.线程池状态为STOP并且工作线程数为0
2)关闭线程池:
cas设置线程池状态为TIDYING成功后(因为前面有自旋,所以到这里了,要么成功,要么到第一步判断线程池已经关闭了就直接返回),执行terminated()方法(子类实现),最后将线程池状态设为TERMINATED

写到这里,其实shutdown方法主要就是对空闲线程进行了中断(执行thread.interrupt方法),那到底中断后会怎么样呢?

中断空闲线程

首先我们回顾一下runWorker方法,代码如下:

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

上一篇讲过,runWorker方法就是循环获取任务,执行任务。前面又说过,线程的空闲和工作状态通过worker实现的不可重入锁来区分,runWorker方法一旦获取到任务,就会调用w.lock()方法,将线程变为工作状态。所以要中断的空闲线程一定是调用w.lock()方法之前,主要是正在获取任务时(获取到任务后,调用w.lock()方法之前这种特殊情况后面再分析)。所以我们要分析在调用getTask方法时线程被中断会发生什么?

先看下getTask方法的代码:

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

这里分两种情况
1)第一种情况:代码执行到if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) 这里时,线程池状态已经为SHUTDOWN了,这时,如果等待队列为空,线程就直接被干掉了
2)第二种情况:代码执行到Runnable r = timed ?workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :workQueue.take();如果任务队列为空,线程便阻塞。这时,shutdown方法将线程池状态变为SHUTDOWN,并且执行了中断方法。那么线程中断会让workQueue.poll或者workQueue.take在阻塞中唤醒(之前讲condition队列时讲过,如果中断发生在signal前则抛异常,如果中断发生在signal之后不抛异常,但终归都是被唤醒了),唤醒之后如果获取不到任务,又会循环进入第一种情况
综上,shutdown方法中断空闲线程,就是在任务队列为空时,让获取不到任务的空闲线程立刻被干掉(那些被阻塞的线程会被立刻唤醒),这样印证了前面介绍的SHUTDOWN状态下不接收新任务,但能处理已添加的任务

2.shutdownNow()

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

跟shutdown方法差不多:
1)advanceRunState(STOP):自旋+CAS将线程池的状态修改为STOP;
2)interruptWorkers():中断所有线程
3)tasks = drainQueue():将队列里还没有执行的任务放到列表里,返回给调用方
4)tryTerminate():向TERMINATED状态转变

这里我们只分析中断所有线程的过程,先说中断空闲线程:

中断空闲线程

上面分析过了shutdown方法中断空闲线程,shutdownNow方法中断空闲线程的逻辑也类似
1)如果在判断if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty()))之前线程池状态变为了STOP,那么不论任务队列中是否有任务,都会被停止。这也印证了前面说的STOP状态下不接收新任务,不处理已添加的任务。
2)如果在之后,除非线程已经获取到了任务,否则也会被停止
很容易理解,就不展开了

再说中断正在工作的线程:

中断正在工作的线程

还是要从runWorker方法说起,上面粘贴过代码,这里只粘贴一部分

while (task != null || (task = getTask()) != null) {
				//将线程状态修改为工作状态
                w.lock();
                // 这里的判断是为了保证在状态为SHUTDOWN时,即使被中断,也能继续执行任务
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }

前面说过,正在工作的线程就是执行完w.lock()的线程,那么会有2种情况
1)shutdownNow方法的中断工作线程发生在执行完w.lock()后,调用task.run()之前
2)shutdownNow方法的中断工作线程发生在调用task.run()之后
我们知道,如果任务要在被中断后结束线程,那么必须在任务代码中手动判断中断信号,比如判断有中断信号后抛错,或者调用能响应中断的方法,比如Thread.sleep(),lock.lockInterruptly在中断后会抛错,所以,针对上面两种情况
1)第一种情况,如果task.run()能响应中断,那么会被中断,如果不能响应,则不能中断
2)第二种情况,不论task.run()能不能响应中断,都不会被中断

我还是上一个示例代码吧

public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            Runnable task = () -> {
                // 模拟任务执行
                try {
                    Thread.sleep(2000);
//                    for (int j = 0; j < 10000000; j++) {
//                        if (j == 9999999)
//                            System.out.println(Thread.currentThread().getName() + ":" + j);
//                    }
                } catch (Exception e) {
                    // 处理中断异常
                    System.out.println("任务被中断。");
                    return;
                }
                System.out.println("任务正在执行。");
            };
            executorService.execute(task);
        }

        // 关闭ExecutorService,并试图停止所有正在执行和暂停的任务
        executorService.shutdownNow();

        // 等待所有任务都停止
        while (!executorService.isTerminated()) {
        }
        System.out.println("服务已关闭。");
    }

执行结果

任务被中断。
任务被中断。
任务被中断。
任务被中断。
任务被中断。
服务已关闭。

如果把任务改成如下:

// 模拟任务执行
                try {
//                    Thread.sleep(2000);
                    for (int j = 0; j < 10000000; j++) {
                        if (j == 9999999)
                            System.out.println(Thread.currentThread().getName() + ":" + j);
}                    

执行结果为:

pool-1-thread-1:9999999
任务正在执行。
pool-1-thread-4:9999999
任务正在执行。
pool-1-thread-2:9999999
任务正在执行。
pool-1-thread-5:9999999
任务正在执行。
pool-1-thread-3:9999999
任务正在执行。
服务已关闭。

显然,因为for循环中的代码并不能响应中断,所以会继续执行完

再把代码调整一下,改为用shutdown方法,任务还是调用thread.sleep:

try {
                    Thread.sleep(2000);     
                }
//中间省略...              
executorService.shutdown();

执行结果为:

任务正在执行。
任务正在执行。
任务正在执行。
任务正在执行。
任务正在执行。
任务正在执行。
任务正在执行。
任务正在执行。
任务正在执行。
任务正在执行。
服务已关闭。

可以看到,shutdown方法会把任务队列中的任务继续执行完

3.runWorker方法的深究

到这里,其实这篇文章已经可以结束了,但是我就怕有些人喜欢跟我一样深究,因为runWorker方法中有一段代码困扰了我很久,如下

if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();

先说结论,我的理解是:这里的判断是为了保证在状态为SHUTDOWN时,即使被中断,也能继续执行完任务

咋一看,有点难以理解,我们慢慢分析:

因为前面在分析shutdown方法的时候说过,有一种极端情况,获取到任务后,调用w.lock()方法之前也属于空闲线程,那么这时候也会被中断,但是前面说过线程池的SHUTDOWN状态是需要处理任务队列中的任务的,既然任务队列中的任务都能处理,那么已经获取到任务后更应该能处理啊,此时线程就不应该被中断了啊,所以这段代码中Thread.interrupted() 就将中断标记清除了,同时也不影响STOP状态下的中断,因为STOP状态下,又通过 wt.interrupt()重新标记上了。这样,就保证了在状态为SHUTDOWN时,即使被中断,也能继续执行任务(ps:可能有杠精说,为啥要保证在状态为SHUTDOWN时,即使被中断,也能继续执行完任务,中断了这个任务就不执行了呗,^_^,人家写这个框架的李二狗就是这样划分线程池状态的,SHUTDOWN状态下就是要能执行任务)

这一节只针对喜欢深究的人,不深究的就跳过

总结

  1. worker实现了AQS的不可重入锁来表示空闲和工作状态
  2. shutdown方法中断空闲线程,就是在任务队列为空时,让获取不到任务的空闲线程立刻被停止
  3. shutdownNow方法中断线程时,对于空闲线程,除非线程已经获取到了任务,否则不论任务队列是否有任务,都会被停止;
  4. shutdownNow方法中断线程时,对于正在工作的线程,如果task.run()能响应中断,且中断发生在响应中断的代码之前,那么会被停止,否则不能停止;如果task.run()不能响应中断,则线程不能停止
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值