前言

相信大家在面试的时候经常会遇到「线程池」相关的问题,比如:

  1. 什么是线程池?线程池的优点?
  2. 有哪几种创建线程池的方式?
  3. 四种创建线程池的使用场景?
  4. 线程池的底层原理?
  5. 线程池相关的参数,比如CorePoolSize、maximunPoolSize、keepAliveTime等等
  6. 为什么阿里巴巴不允许线程池使用Executors去创建?
  7. 如何合理的设置线程池参数的等等等等…

如上这些问题,如果看过线程池源码的小伙伴,基本就能回答上来了,即便是看面试题也能说上个一二,但是当真的问你,如何关闭线程池?你能回答上来吗~

其实这个问题直接关乎到你到底用没用过线程池,可以想象一下,当面试官问你用没用过线程池,如果你回答用过,还头头是道的说了一下如何如何创建,各个有哪些使用场景,底层有哪些参数等等,但此时问你,线程池如何关闭呢?两种关闭方法有什么区别呢?调用线程池关闭方法时线程池里的线程会有什么反应呢?

这时,会不会很尴尬呢~,哦,会创建不会关闭呢。

正文,文末总结

今天我们不关心其他问题,就看如何关闭线程池,防止被面试官打个措手不及。

关闭线程池有两个方法,分别是:shutdown()、shutdownNow()

  • shutdownNow():调用该方法后,首先将线程池的状态设置为 stop,线程池拒绝接受新任务的提交,然后尝试停止所有正在执行或者暂停任务的线程(也就是线程池里现有的任务也不再执行),并返回等待执行任务的列表。。
  • shutdown():调用该方法后,会将线程池的状态设置为 shutdown,线程池拒绝接受新任务的提交,同时等待线程池内的任务执行完毕之后再关闭线程池。

看完这两个方法的解释,有一个小的结论:调用两个方法后都不会再接收新的任务,调用 shutdownNow() 会 “立刻” 停止线程池里所有的线程(注意,这里的立刻用的双引号,后面会否定这个立刻的),会返回等待执行的任务列表;调用 shutdown() 则会等待线程池里的任务执行完毕之后再关闭线程池,无返回值。

无码无真相,我们通过代码去看看这两个方法(JDK1.8 + ThreadPoolExecutor):

1、shutdownNow()
public List<Runnable> shutdownNow() {List<Runnable> tasks;final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {checkShutdownAccess();  1、检查线程,是否有权限修改advanceRunState(STOP);  2、修改线程池的状态为STOP状态interruptWorkers();     3、遍历线程池里的所有工作线程,然后调用线程的interrupt方法tasks = drainQueue();   4、将队列里还没有执行的任务放到列表里,返回给调用方} finally {mainLock.unlock();}tryTerminate();return tasks;
}

我们主要关注 try/catch 里的 4 行代码:

  1. checkShutdownAccess() :检查线程,是否有权限修改

  2. advanceRunState(STOP):修改线程池的状态为STOP状态

  3. interruptWorkers():遍历线程池里的所有工作线程,然后调用线程的interrupt方法

  4. drainQueue():将队列里还没有执行的任务放到列表里,返回给调用方

这里我们额外看一下第三步 interruptWorkers() 方法,这可能是一些熟悉的东西:

private void interruptWorkers() {final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {for (Worker w : workers)w.interruptIfStarted();  这是这段代码的重点} finally {mainLock.unlock();}
}

可以看到一个 for 循环,然后调用 interruptIfStarted() 方法, 还是不熟悉怎么办?没关系的,我们接着往下看 interruptIfStarted() 方法:

void interruptIfStarted() {Thread t;if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {try {t.interrupt();  这是这段代码的重点} catch (SecurityException ignore) {}}
}

看到没,终究还是调的线程的 interrupt() 方法,不熟悉如何关闭线程的小伙伴可以移步这里 > 如何暂停一个正在运行的线程?

先临时总结一下 shutdownNow() 方法的执行逻辑:将线程池状态修改为 STOP,然后遍历线程池里的工作线程,逐个调用线程的 interrupt() 方法来中断线程,因为是调用的 interrupt() 方法,所以线程并不会立刻执行结束,只是给线程设置了标志位,至于什么时候真的中断线程需要看 getTask() 方法的返回是否为 null 了(后面详看 getTask() 方法)。

新的问题来了,我们再来看调用 shutdownNow() 方法后,线程池的线程会做如何反应,此时我们需要看一下线程池里的 runWorker() 方法,先给不太了解线程池运行过程的小伙伴补充一下流程:

线程池调用execute提交任务 —> 创建Worker(设置属性thead、firstTask)—> worker.thread.start() —> 实际上调用的是 worker.run() —> 线程池的runWorker(worker) —> worker.firstTask.run()

总之,运行线程池的就是在 runWorker() 方法里:

final void runWorker(Worker w) {Thread wt = Thread.currentThread();Runnable task = w.firstTask;w.firstTask = null;w.unlock(); // allow interruptsboolean completedAbruptly = true; // 标志是不是用户任务异常导致终止的try {// 这里通过循环,不断地取任务来执行,getTask() 是会阻塞的while (task != null || (task = getTask()) != null) {w.lock();// stop 状态时不接受新任务,不执行已经加入任务队列的任务,还中断正在执行的任务// 所以对于 stop 状态以上是要中断线程的// (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP)确保线程中断标志位为true且是stop状态以上,接着清除了中断标志// !wt.isInterrupted()则再一次检查保证线程需要设置中断标志位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++;// 完成数+1w.unlock();}}completedAbruptly = false;// 标记不是用户任务异常引起的} finally {processWorkerExit(w, completedAbruptly);}
}

正常情况下,线程池里的线程,通过 while 循环不停的执行任务,其中 task.run() 方法就是执行任务的关键代码,当我们调用了 shutdownNow() 方法时,task.run() 方法里面正处于IO阻塞时,则会导致报错,如果 task.run() 方法里正在正常执行,则不受影响,继续执行完这个任务。

还有一种情况,getTask() 方法返回 null 时,也会导致线程的退出。

private Runnable getTask() {boolean timedOut = false; // 取任务是否超时for (;;) {int c = ctl.get();int rs = runStateOf(c);// 这个状态判断挺重要的,起到线程池关闭作用if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {decrementWorkerCount();// 线程数量减一return null;// 这里返回null,意味着一个线程会退出}int wc = workerCountOf(c);// 这里可以看出核心线程在空闲的时候也是可以设置被回收的// timed为true将要有时间限制地取任务boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;if ((wc > maximumPoolSize || (timed && timedOut))&& (wc > 1 || workQueue.isEmpty())) {// 大于最大限制线程数或超过空闲时间,并且当前线程数大于1或队列为空if (compareAndDecrementWorkerCount(c))return null;// 说明线程数减一成功,返回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;}}}

这个 getTask() 过程就是,当我们调用 shutdownNow() 方法时,如果线程正处于从队列中读取任务( Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); )而发生阻塞,这样会导致抛出 InterruptedException 异常,但是这个异常被 try/catch 捕获掉了,同时设置了 timedOut 标志位,线程将会继续进入下一个 for 循环里继续执行。

但因为 shutdownNow() 方法将线程状态设置为 STOP,所以当执行到下一个 for 循环的第一个 if语句 if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) 时,STOP 满足 >= SHUTDOWN,而 STOP 也满足 >= STOP,所以这个地方就尤为重要了,这个时候会进入 if 语句体中,返回 null ,然后线程退出。

至此,总结一下,调用 shutdownNow() 方法时,程池里的线程会有什么反应?

会有两种情况退出线程。

当我们调用 shutdownNow() 方法时,如果线程池正在 getTask() 方法中执行,就会通过 for 循环进入 if 语句,判断条件是 标志位 >= SHUTDOWN,或者 标志位 >= STOP,因为符合条件所以会返回 null,然后线程退出。

while (task != null || (task = getTask()) != null)

再就是线程执行提交任务到线程池时而处于阻塞状态,就会导致报错抛出 InterruptedException 异常;处于正常运行状态下则会执行完当前任务,然后通过 getTask() 方法返回 null 来退出。

2、shutdown()
public void shutdown() {final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {checkShutdownAccess();     1、检查线程,是否有权限修改advanceRunState(SHUTDOWN); 2、修改线程池的状态为STOP状态interruptIdleWorkers();    3、遍历线程池里的所有工作线程,然后调用线程的interrupt方法onShutdown();              4、留给子类具体实现,如 ScheduledThreadPoolExecutor} finally {mainLock.unlock();}tryTerminate();
}

从上边 shutdownNow() 捋下来后,会发现 shutdown() 方法非常的相似:

  1. checkShutdownAccess():检查线程,是否有权限修改
  2. advanceRunState(SHUTDOWN):修改线程池的状态为STOP状态
  3. interruptIdleWorkers():遍历线程池里的所有工作线程,然后调用线程的interrupt方法
  4. onShutdown():留给子类具体实现,如 ScheduledThreadPoolExecutor

具体方法细节就不重复了,大致过程就是:shutdown() 方法会修改线程状态为 SHUTDOWN 状态,然后调用 interruptIdleWorkers() 方法来中断空闲线程,这个过程也是同样的遍历线程池里的工作线程,逐个调用线程的 interrupt() 方法,至于什么时候真的中断线程需要看 getTask() 方法的返回是否为 null 了。

然后就是:调用 shutdown() 方法时,程池里的线程会有什么反应?

if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {

在 getTask() 里的 if 判断中,由于线程池被 shutdown() 方法修改为 SHUTDOWN 状态,SHUTDOWN >= SHUTDOWN 条件成立,而逻辑且 & 的后半条件只有在队列为空,getTask() 方法才会返回 null,然后线程退出。

最后总结

关闭线程池有两种方法,分别是 shutdown()、shutdownNow(),调用两者都会让线程池不再接受新的任务,并且他们的原理都是遍历线程池中的工作线程,然后逐个调用线程的 inputter() 方法来中断线程,而两者的区别是,调用 shutdownNow() 会将线程池设置为 STOP 状态,该方法会返回等待执行的任务列表,而调用 shutdown() 方法会将线程池设置为 SHUTDOWN 状态,无返回值。

调用了shutdown()、shutdown() 时,线程池里的线程会有什么反应?

会有两种情况退出线程。

  1. 当我们调用关闭线程池方法时,如果线程池正在 getTask() 方法中执行,就会通过 for 循环进入 if 语句,判断线程池状态是否满足中断线程,如果满足就会返回 null,然后线程退出。

  2. 再就是线程执行提交任务到线程池时而处于阻塞状态时,就会导致报错抛出 InterruptedException 异常,线程退出;处于正常运行状态下则会执行完当前任务,然后通过 getTask() 方法返回 null 来退出。

额外补充一:

无论是 shutdownNow() 还是 shutdown(),由于原理都是调用单个线程的 interrupt() 方法,所以并不是直接就结束线程池的,而是通知线程池接下来的做法,但具体什么时间执行就不知道了,如何判断线程池真的关闭了可以调用 isTerminaed() 方法,返回 true 则表示关闭成功。

额外补充二:

如果需要同步等待线程池彻底关闭后才继续往下执行,需要调用 awaitTermination() 方法进行同步等待。其实实际开发过程中这种情景也是存在的,我在这举个简单的例子:

组合文件下载,就是用户有2个及以上的文件下载需求,但是为了考虑用户体验,希望最终返回给用户的是一个压缩包文件(内嵌好几个小文件),所以为了快速首先是在后台采用了多线程的方式下载文件,然后用 awaitTermination() 方法同步等待,将最终的结果压缩为一个文件返回给用户。

博客园持续更新,欢迎大家订阅关注,未来,我们一起成长。

本文首发于博客园:https://www.cnblogs.com/niceyoo/p/13657538.html

如何关闭线程池?会创建不会关闭?调用关闭方法时线程池里的线程如何反应?相关推荐

  1. Linux下通用线程池的创建与使用

    Linux下通用线程池的创建与使用 本文给出了一个通用的线程池框架,该框架将与线程执行相关的任务进行了高层次的抽象,使之与具体的执行任务无关.另外该线程池具有动态伸缩性,它能根据执行任务的轻重自动调整 ...

  2. 多线程—Thread类及线程三种创建方式及对比

    线程创建的3种方法: 1.继承Thread类并重写run方法 Thread类方法: Thread Thread.currentThread() :获得当前线程的引用.获得当前线程后对其进行操作. Th ...

  3. Qt对话框的事件循环分析(子线程中不能创建UI窗体分析)

    重要: GUI线程和辅助线程 如前所述,每个程序在启动时都有一个线程.这个线程被称为"主线程"(在Qt应用程序中也称为"GUI线程").Qt GUI必须在这个线 ...

  4. Qt对话框的事件循环分析(子线程中不能创建UI窗体分析2)

    Qt事件机制 QT-UI 后端 重要: GUI线程和辅助线程 如前所述,每个程序在启动时都有一个线程.这个线程被称为"主线程"(在Qt应用程序中也称为"GUI线程&quo ...

  5. Python 批量创建线程及threading.Thread类的常用函数及方法

    在<[Python]线程的创建.执行.互斥.同步.销毁>(点击打开链接)中介绍了Python中线程的使用,但是里面线程的创建,使用了很原始的方式,一行代码创建一条.其实,Python里是可 ...

  6. 【Android NDK 开发】JNI 线程 ( JNI 线程创建 | 线程执行函数 | 非 JNI 方法获取 JNIEnv 与 Java 对象 | 线程获取 JNIEnv | 全局变量设置 )

    文章目录 I . JNI 线程创建 II . 线程执行函数 III . 线程方法获取 Java 对象 IV . 线程方法获取 JNIEnv V . JNI 线程 完整代码示例 I . JNI 线程创建 ...

  7. 详解java中Thread类,线程和进程的基本区别,多线程的好处,线程的五个生命周期,主线程和IDEA创建的Monitor Ctrl-Break守护线程;优雅地终止线程。死锁的产生

    线程:CPU调度的最小单位:线程共享堆内存和方法区,但各自享有栈空间.进程:CPU分配资源的最小单位. 为什么需要多线程:①避免其中一个模块阻塞造成系统假死:②程序异步处理(交替执行),提高程序执行效 ...

  8. 如何创建线程?如何创建扩展Thread类?

    大多数情况,通过实例化一个Thread对象来创建一个线程.Java定义了两种方式: · 实现Runnable 接口. · 可以继承Thread类. 前一篇文章介绍了下面介绍了实现Runnable 接口 ...

  9. android 多线程创建texture,从源码角度剖析Android系统EGL及GL线程

    本文转载自天天P图攻城狮微信公众号,作者:天天P图Android工程师kenneyqin(覃华峥),原文链接https://mp.weixin.qq.com/s/j_N5_C7iQUPWENdRYfj ...

最新文章

  1. python数据分析视频网盘-微专业Python数据分析实战
  2. PrepareStatement 和Statement 的区别?
  3. 用java调用.net的wcf其实还是很简单的
  4. 7天后自动更新cookie
  5. Koa2-下载-文件流
  6. java tomcat数据库连接池,tomcat 数据库连接池拿不到连接
  7. 制作JD的手动和自动轮播图片板块
  8. 在线拍卖系统代码_来了来了,轻工业领域拍卖的福利来啦!聚拍网终于扩展新范围啦...
  9. 记录一个手写场景的过程
  10. 线性代数 第二章 矩阵 知识点总结(Jeff自我感悟)
  11. 思科CCNP认证介绍
  12. 条码打印软件如何设置双排标签纸尺寸
  13. 组成原理 - 内存颗粒分类(ram,rom)
  14. 揭秘全美第一黑客组织Anonymous(匿名者)的装备库
  15. HTML笔记(课堂笔记整合)
  16. 美团2023届笔试题解
  17. “快准狠”找到系统内存的问题
  18. 小程序第三方微信授权登录的实现
  19. 仿真软件测试工程师麦克,仿真工程师面试经验 - 共61条真实仿真工程师面试经验分享 - 职业圈...
  20. Python绘制世界疫情地图

热门文章

  1. [html] html标签中的lang属性有什么作用?
  2. [vue] prop是怎么做验证的?可以设置默认值吗?
  3. 工作97:父子组件传值
  4. 前端学习(1494):表格案例--axios-搜索功能
  5. STM32 DSP库的使用方法
  6. 利用闭包实现onclick事件传递参数
  7. python画图灰白_python 站点资料插值画图及白化
  8. c语言八个方向迷宫课程设计,【精品资料最新版】C语言课程设计-迷宫游戏.doc...
  9. mysql存储过程中怎么睡几秒_MySql的逻辑架构
  10. NCoreCoder.Aop详解