Skip to main content

moregeek program

线程池中多余的线程是如何回收的?_码农小宋的博客-多极客编程

最近阅读了JDK线程池​​ThreadPoolExecutor​​的源码,对线程池执行任务的流程有了大体了解,实际上这个流程也十分通俗易懂,就不再赘述了,别人写的比我好多了。

不过,我倒是对线程池是如何回收工作线程比较感兴趣,所以简单分析了一下,加深对线程池的理解吧。

那么,就以JDK1.8为例分析吧。

1. runWorker(Worker w)

工作线程启动后,就进入​​runWorker(Worker w)​​方法。

里面是一个while循环,循环判断任务是否为空,若不为空,执行任务;若取不到任务,或发生异常,退出循环,执行​​processWorkerExit(w, completedAbruptly);​​ 在这个方法里把工作线程移除掉。

取任务的来源有两个,一个是firstTask,这个是工作线程第一次跑的时候执行的任务,最多只能执行一次,后面得从​​getTask()​​方法里取任务。看来,​​getTask()​​是关键,在不考虑异常的场景下,返回null,就表示退出循环,结束线程。下一步,就得看看,什么情况下​​getTask()​​会返回null。

篇幅有限,分段截取,省略中间执行任务的步骤

线程池中多余的线程是如何回收的?_线程池

线程池中多余的线程是如何回收的?_工作线程_02

2. getTask() 返回null

一共有两种情况会返回null,见红框处 。

第一种情况,线程池的状态已经是​​STOP​​,​​TIDYING​​, ​​TERMINATED​​,或者是​​SHUTDOWN​​且工作队列为空;

第二种情况,工作线程数已经大于最大线程数或当前工作线程已超时,且,还有其他工作线程或任务队列为空。这点比较难理解,总之先记住,后面会用。

下面以条件1和条件2分别指代这两种情况的判断条件。

线程池中多余的线程是如何回收的?_线程池_03

3. 分场景分析线程池回收工作线程

3.1 未调用shutdown() ,RUNNING状态下全部任务执行完成的场景

这种场景,会将工作线程的数量减少到核心线程数大小(如果本来就没有超过,则不需要回收)。

比如一个线程池,核心线程数为4,最大线程数为8。一开始是4个工作线程,当任务把任务队列塞满,就得将工作线程增加到8. 当后面任务执行到差不多了,线程取不到任务了,就会回收到4个工作线程的状态(取决于​​allowCoreThreadTimeOut​​的值,这里讨论默认值false的情况,即核心线程不会超时。如果为true,工作线程可以全部销毁)。

可以先排除上面提到的条件1,线程池的状态已经是​​STOP​​,​​TIDYING​​, ​​TERMINATED​​,或者是​​SHUTDOWN​​且工作队列为空。因为线程池一直是​​RUNNING​​,这条判断永远是false。在这个场景中,可以当条件1不存在。

下面分析取不出任务时线程是怎么运行的。

  • step1. 从任务队列取任务有两种方式,超时等待还是可以一直阻塞下去。决定因素是timed变量。该变量在前面赋值,如果当前线程数大于核心线程数,变量timed为true, 否则为false(上面说了,这里只讨论allowCoreThreadTimeOut为false的情况)。很明显,现在讨论的是timed为true的情况。keepAliveTime一般不设置,默认值为0,所以基本上可以认为是不阻塞,马上返回取任务的结果。

在线程超时等待唤醒之后,发现取不出任务,timeOut变为true,进入下一次循环。

  • step2. 来到条件1的判断,线程池一直RUNNING, 不进入代码块。
  • step3. 来到条件2的判断,这时任务队列为空,条件成立,CAS减少线程数,若成功,返回null,否则,重复step1。

这里要注意,有可能多条线程同时通过条件2的判断,那会不会减少后线程的数量反而比预想的核心线程数少呢?

比如当前线程数已经只有5条了,此时有两条线程同时唤醒,通过条件2的判断,同时减少数量,那剩下的线程数反而只有3条,和预期不一致。

实际上是不会的。为了防止这种情况,​​compareAndDecrementWorkerCount(c)​​ 用的是CAS方法,如果CAS失败就continue,进入下一轮循环,重新判断。

像上述例子,其中一条线程会CAS失败,然后重新进入循环,发现工作线程数已经只有4了,timed为false, 这条线程就不会被销毁,可以一直阻塞了(​​workQueue.take()​​)。

这一点我思考了很久才得出答案,一直在想没有加锁的情况下是怎么保证一定能不多不少回收到核心线程数的呢。原来是CAS的奥妙。

从这里也可以看出,虽然有核心线程数,但线程并没有区分是核心还是非核心,并不是先创建的就是核心,超过核心线程数后创建的就是非核心,最终保留哪些线程,完全随机。

3.2 调用shutdown() ,全部任务执行完成的场景

这种场景,无论是核心线程还是非核心线程,所有工作线程都会被销毁。

在调用​​shutdown()​​之后,会向所有的空闲工作线程发送中断信号。

线程池中多余的线程是如何回收的?_任务队列_04

最终传入false,调用下面这个方法。

线程池中多余的线程是如何回收的?_线程池_05

可以看出,在发出中断信号前,会判断是否已经中断,以及要获得工作线程的独占锁。

发出中断信号的时候,工作线程要么在​​getTask()​​里准备获取任务,要么在执行任务,那就得等它执行完当前任务才会发出,因为工作线程在执行任务的时候,也会工作线程加锁。工作线程执行完任务,又跑到​​getTask()​​里面去了。

所以我们只要看​​getTask()​​里面怎么应对中断异常的就可以了。

线程池中多余的线程是如何回收的?_任务队列_06

工作线程在​​getTask()​​里,有两种可能。

3.2.1 任务已全部完成,线程在阻塞等待。

很简单,中断信号将其唤醒,从而进入下一轮循环。到达条件1处,符合条件,减少工作线程数量,并返回null,由外层结束这条线程。

这里的​​decrementWorkerCount()​​是自旋式的,一定会减1。

线程池中多余的线程是如何回收的?_工作线程_07

3.2.2 任务还没有完全执行完

调用​​shutdown()​​之后,未执行完的任务要执行完毕,池子才能结束。所以此时有可能线程还在工作。

这里又要分两个阶段讨论

阶段1 任务较多,工作线程都能获得任务

这里还不涉及到线程退出,可以跳过不看,只是分析一下收到中断信号后线程的表现。

假设有线程A,正通过​​getTask()​​里获取任务。此时A被中断,在获取任务时,无论是​​poll()​​还是​​take()​​,都会抛出中断异常。异常被捕获,重新进入下一轮循环,只要队列不为空,就可以继续取任务。

线程A被中断,再次取任务,调用​​workQueue.poll()​​ or ​​workQueue.take()​​,不会抛出异常吗?还可以正常取出任务吗?

这就要看​​workQueue​​的实现了。​​workQueue​​是​​BlockingQueue​​类型,以常见的​​LinkedBlockingQueue​​和​​ArrayBlockingQueue​​为例,加锁时都是调用​​lockInterruptibly()​​,是响应中断的。该方法又调用了AQS的​​acquireInterruptibly(int arg)​​。

​acquireInterruptibly(int arg)​​,无论是在入口处判断中断异常,还是在​​parkAndCheckInterrupt()​​方法阻塞,被中断唤醒并判断中断异常时,均使用了​​Thread.interrupted()​​。这个方法会返回线程的中断状态,并把中断状态重置!也就是说,线程不再是中断状态了,这样在再次取任务时,就不会报错了。

因此,这对于正在准备取任务的线程,只是相当于浪费了一次循环,这可能是线程中断带来的副作用吧,当然,对整体的运行不影响。

分析到这里,我不禁感叹,这里​​BlockingQueue​​刚好是会重置中断状态,这到底是怎么想出来的绝妙设计啊?Doug Lea大神Orz。

线程池中多余的线程是如何回收的?_工作线程_08

线程池中多余的线程是如何回收的?_线程池_09

阶段2 任务刚好要执行完了

这时任务已经快取完了,比如有4条工作线程,只剩下2个任务,那就可能出现2条线程获得任务,2条线程阻塞。

因为在获取任务前的判断,没有加锁,那么会不会出现,所有线程都通过了前面的校验,来到workQueue获取任务的地方,刚好任务队列已经空了,线程全部阻塞了呢?因为​​shutdown() ​​已经执行完毕,无法再向线程发出中断信号,从而线程一直在阻塞,无法被回收。

这种是不会发生的。

假设有A,B,C,D四条工作线程,同时通过了条件1和条件2的判断,来到取任务的地方。那么,工作队列至少还有一个任务,至少会有一条线程能取到任务。

假设A,B获得了任务,C,D阻塞。

A, B接下来的步骤是:

  • step1. 任务执行完成后,再次getTask(),此时符合条件1,返回null,线程准备被回收。
  • step2. processWorkerExit(Worker w, boolean completedAbruptly) 将线程回收。

回收就只是把线程干掉这么简单吗?来看看​​processWorkerExit(Worker w, boolean completedAbruptly)​​ 的方法。

线程池中多余的线程是如何回收的?_线程池_10

可以看到,在里面除了​​workers.remove(w)​​ 移除线,还调用了​​tryTerminate()​​。

线程池中多余的线程是如何回收的?_工作线程_11

第一个判断条件没有一个子条件符合,跳过。第二个条件,工作线程还存在,那么随机中断一条空闲线程。

那么问题就来了,中断一条空闲线程,也没说是一定中断正在阻塞的线程啊。如果A, B同时退出,有没有可能出现A中断B, B中断A,AB互相中断,从而没有线程去中断唤醒阻塞的线程呢?

答案仍然是,想多了……

假设A能走到这里,说明A已经从工作线程的集合workers里面移除了(​​processWorkerExit(Worker w, boolean completedAbruptly)​​ 在​​tryTerminate()​​之前,已经将其移除)。那么A中断B,B来到这里中断,就不会在workers里面找到A了。

线程池中多余的线程是如何回收的?_工作线程_12

也就是说,退出的线程不能互相中断,我从集合中退出后,中断了你,你不能中断我,因为我已经退出集合,你只能中断别人。那么,即使有N个线程同时退出,至少在最后,也会有一条线程,会中断剩余的阻塞线程。

就像多米诺骨牌一样,中断信号就会被传播下去。

阻塞的C,D中的任意一条被中断唤醒后,又会重复step1的动作,周而复始,直到所有阻塞线程都被中断,唤醒。

这也是为什么在​​tryTerminate()​​里面,传入false,只需要中断任意一条空闲线程的原因。

想到这里,再次对Doug Lea心生钦敬(粤语)之情。这设计得也太妙了叭。

4. 总结

​ThreadPoolExecutor​​回收工作线程,一条线程​​getTask()​​返回null,就会被回收。

分两种场景。

1) 未调用shutdown() ,RUNNING状态下全部任务执行完成的场景

线程数量大于​​corePoolSize​​,线程超时阻塞,超时唤醒后CAS减少工作线程数,如果CAS成功,返回null,线程回收。否则进入下一次循环。当工作者线程数量小于等于​​corePoolSize​​,就可以一直阻塞了。

2) 调用shutdown() ,全部任务执行完成的场景

​shutdown() ​​会向所有线程发出中断信号,这时有两种可能。

2.1)所有线程都在阻塞

中断唤醒,进入循环,都符合第一个if判断条件,都返回null,所有线程回收。

2.2)任务还没有完全执行完

至少会有一条线程被回收。在​​processWorkerExit(Worker w, boolean completedAbruptly)​​方法里会调用​​tryTerminate()​​,向任意空闲线程发出中断信号。所有被阻塞的线程,最终都会被一个个唤醒,回收。

这一次的分析,昨晚开始写,写到一半卡壳,今天早上接着写,前后花了大概2+2=4个小时写博客以及1小时思考。

说实话自己还是有点乱,无法一下子理解透彻,也不知道自己理解得对不对。

有没有用,我也不知道,只能说,加深了对线程池的理解吧(安慰自己),同时也感慨设计之精妙。

如有不正确的地方,请大家指正。

©著作权归作者所有:来自51CTO博客作者码农小宋的原创作品,请联系作者获取转载授权,否则将追究法律责任

mybatisplus lambda表达式 聚合查询 分组查询 count sum avg min max groupby_赛先生和泰先生的博客-多极客编程

一、序言 众所周知,MybatisPlus在处理单表DAO操作时非常的方便。在处理多表连接连接查询也有优雅的解决方案。今天分享MybatisPlus基于Lambda表达式优雅实现聚合分组查询。 由于视频的交互性更强,保留更多的细节,看视频的朋友,传送门在这里。 下面的内容是博客文字版。 二、代码实现 1、用户实体类 @TableName(value = "tb_user") public clas

java中线程的生命周期_wx630f055ce23fc的博客-多极客编程

Java中线程的生命周期1 介绍本篇文章我们讨论下Java中的一个非常核心的概念:线程的生命周期。在Java编程语言中,多线程编程非常重要。线程从创建到销毁是有生命周期的,在线程的生命周期中,线程会经历多种状态(state)。2 线程状态说明以及状态流转图​​java.lang.Thread​​类中有一个静态的枚举类:​​State​​,用来表示线程各种可能的状态:public enum Stat

初识微服务(技术栈、单体、分布式架构)、springcloud_perceus.的博客-多极客编程

(目录) 微服务技术栈 从单体架构过度到微服务架构,需要一系列中间技术支撑,其中重要的部分包括: 注册中心:Eureka 、Zookeeper、Nacos 服务网关:Zuul 、Gateway 微服务远程调用:RestTemplate、Feign 容器化技术 Docker 消息队列 MQ(多种实现方式) 负载均衡 Ribbon 、 Nginx 分布式搜索技术:ElasticSearch

#yyds干货盘点# 动态规划专题:跳跃游戏(三)_风的博客-多极客编程

1.简述:描述给定一个非负整数数组nums,假定最开始处于下标为0的位置,数组里面的每个元素代表下一跳能够跳跃的最大长度。请你判断最少跳几次能跳到数组最后一个位置。1.如果跳不到数组最后一个位置或者无法跳跃(即数组长度为0),请返回-12.数据保证返回的结果不会超过整形范围,即不会超过数据范围:输入描述:第一行输入一个正整数 n 表示数组 nums的长度第二行输入 n 个整数,表示数组 nums

一步步带你设计mysql索引数据结构_wx630f055ce23fc的博客-多极客编程

前言MySQL的索引是一个非常重要的知识点,也基本上是面试必考的一个技术点,所以非常重要。那你了解MySQL索引的数据结构是怎么样的吗?为什么要采用这样的数据结构?现在化身为MySQL的架构师,一步步迭代设计出MySQL的索引结构,保证你再也忘记不了索引的结构了,轻松通过面试。索引介绍MySQL表中存储的数据量非常大,可能有上亿条记录,如果一条条去匹配,就是所谓的全表扫描,会非常的慢。那么有什么办

optional用法与争议点_wx630f055ce23fc的博客-多极客编程

简介要说Java中什么异常最容易出现,我想NullPointerException一定当仁不让,为了解决这种null值判断问题,Java8中提供了一个新的工具类Optional,用于提示程序员注意null值,并在特定场景中简化代码逻辑。比如下面一段取深层属性值的代码:Order order = getOrderById(orderId);String userCode = "";if(order

mybatisplus lambda表达式 聚合查询 分组查询 count sum avg min max groupby_赛先生和泰先生的博客-多极客编程

一、序言 众所周知,MybatisPlus在处理单表DAO操作时非常的方便。在处理多表连接连接查询也有优雅的解决方案。今天分享MybatisPlus基于Lambda表达式优雅实现聚合分组查询。 由于视频的交互性更强,保留更多的细节,看视频的朋友,传送门在这里。 下面的内容是博客文字版。 二、代码实现 1、用户实体类 @TableName(value = "tb_user") public clas

c++ 不知图系列之基于邻接矩阵实现广度、深度搜索_一枚大果壳的博客-多极客编程

1. 前言 图是一种抽象数据结构,本质和树结构是一样的。 图与树相比较,图具有封闭性,可以把树结构看成是图结构的基础部件。在树结构中,如果把兄弟节点之间或子节点之间横向连接,便构建成一个图。 树适合描述从上向下的一对多的数据结构,如公司的组织结构。 图适合描述更复杂的多对多数据结构,如群体社交关系、城市交通路线…… 本文将讨论以邻接矩阵方式存储图,并在此基础之上对图进行深度、广度搜索。 2.

flask框架:运用ajax轮询动态绘图_lyshark的博客-多极客编程

Ajax是异步JavaScript和XML可用于前后端交互,在之前《Flask 框架:运用Ajax实现数据交互》简单实现了前后端交互,本章将通过Ajax轮询获取后端的数据,前台使用echart绘图库进行图形的生成与展示,后台通过render_template方法返回一串JSON数据集,前台收到后将其应用到绘图库上,实现动态监控内存利用率的这个功能。 首先LyShark先来演示一下前端如何运用AJA

#yyds干货盘点# 动态规划专题:跳跃游戏(三)_风的博客-多极客编程

1.简述:描述给定一个非负整数数组nums,假定最开始处于下标为0的位置,数组里面的每个元素代表下一跳能够跳跃的最大长度。请你判断最少跳几次能跳到数组最后一个位置。1.如果跳不到数组最后一个位置或者无法跳跃(即数组长度为0),请返回-12.数据保证返回的结果不会超过整形范围,即不会超过数据范围:输入描述:第一行输入一个正整数 n 表示数组 nums的长度第二行输入 n 个整数,表示数组 nums

一步步带你设计mysql索引数据结构_wx630f055ce23fc的博客-多极客编程

前言MySQL的索引是一个非常重要的知识点,也基本上是面试必考的一个技术点,所以非常重要。那你了解MySQL索引的数据结构是怎么样的吗?为什么要采用这样的数据结构?现在化身为MySQL的架构师,一步步迭代设计出MySQL的索引结构,保证你再也忘记不了索引的结构了,轻松通过面试。索引介绍MySQL表中存储的数据量非常大,可能有上亿条记录,如果一条条去匹配,就是所谓的全表扫描,会非常的慢。那么有什么办

optional用法与争议点_wx630f055ce23fc的博客-多极客编程

简介要说Java中什么异常最容易出现,我想NullPointerException一定当仁不让,为了解决这种null值判断问题,Java8中提供了一个新的工具类Optional,用于提示程序员注意null值,并在特定场景中简化代码逻辑。比如下面一段取深层属性值的代码:Order order = getOrderById(orderId);String userCode = "";if(order