线程池中的 工作线程如何被回收
做积极的人,而不是积极废人!
每天 14:00 更新文章,每天掉亿点点头发...
源码精品专栏
JDK中的ThreadPoolExecutor线程池相信大家都很熟悉,对于线程池的一些高频面试题,比如有哪些参数,每个参数的含义,什么时候发挥作用,工作流程等问题都能回答上来。而对于一些不是很常见的线程池面试题就显得有点模糊,比如:线程池中线程执行完了一个任务接下来是做什么,是等待还是被收回,如果是等待,那么判断的依据是啥,如果是被回收,那么是怎么被回收的。对于这些问题我们就必须深挖ThreadPoolExecutor源码知识了,而不是背几个常见的面试题就行,下面我们一起来看一下线程执行完了一个任务接下来是做什么
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能。
execute方法是我们使用线程池的源头,我来看一下调用execute方法后干了什么
重点看addWorker()方法,这里我们要联想到线程池是一个池子,从方法名也可以看出addWorker()增加一个工作者,那么这个Worker对象肯定会放到池子里面去,我们能想到这个池子肯定是一个集合,比如List,Set,Map都可以
果然是用Set做集合,然后会把Worder对象放到Set中,然后我们再来看是怎么向线程池中添加任务的。
我们会看到将传进来的Runnable任务包装成一个Worker对象,然后将Worker的thread成员属性复制给了Thread t局部变量,我们具体看一下Worker这个类
会发现Worker的构造方法会利用创建线程工厂创建一个新线程,并且将当前this对象赋值给了thread成员变量,重写了run()方法,也至于上一张图所指出的thread.start()启动线程会调用Worker对象run()方法,后面重点来看run()方法
基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。
从源码上可以看到重写的run()方法主要调用的就是runWorker方法
可以看到runWorker方法中,首先会将firstTask赋值给Runnable task变量,firstTask是工作线程第一次跑的时候执行的任务,然后随后就将firstTask=null,即firstTask只会执行一次。接下来就进入while条件循环,如果while条件循环不满足,最后就会进入到finally中的processWorkerExit线程退出,也就从线程池Set中remove线程。所以我们可以把重点关注在while的条件上,第一个条件task != null是用来判断第一次跑的任务,后面的getTask()方法里获取任务,接下来就看getTask()方法,看看getTask()方法什么时候给我们返回null,就代表着这个while条件结束,也就会进入到我们的processWorkerExit()线程退出方法
我们可以看到有两个地方返回null,一个是当线程池的状态是STOP,TIDYING, TERMINATED,或者是SHUTDOWN且工作队列为空,那么就会返回null,decrementWorkerCount()将工作线程数量减1。这里暂时先不考虑线程池中止状态,先假设线程池一直都是RUNNING状态,那么就会进入到第二个返回null的判断条件。第二个地方返回null的条件是:1. 线程池中的工作线程数量 > 最大线程数 or (大于核心线程数 and 工作线程存活时间已经超时) 2. 线程数 > 1 or 队列已经为空 同时满足条件1,2时,再调用CAS扣减线程数,这里解释一下为什么需要用到CAS扣减线程,因为防止两个线程同时满足条件,然后扣减线程数,这样会导致线程数变少。比如核心线程数为4,当前线程数有5个,然后有两个线程判断条件,发现同时满足,则两个线程都会进行扣减,变成3,本来应该保持核心线程数为4的。所以采用CAS操作来扣减线程达到核心线程数,如果扣减成功,则返回null,也就会结束runWorker()方法中的while条件。然后进入finally区域,退出线程
前面分析到第二个地方返回null指的是线程正常执行完一个任务,然后从getTask()获取任务,当获取任务为null时,就会退出线程了,从线程池中删除。下面来分析一下线程池状态为STOP,TIDYING, TERMINATED,或者是SHUTDOWN且工作队列为空时,返回null的情况。
当调用shutdown()方法时,线程池会等待正在执行任务的线程并且需要将阻塞队列中的任务执行完再进行销毁,并且不再接受新任务。跟shutdownNow()方法不同的是,shutdownNow方法不管是正在执行还是空闲的线程都会进行中断,返回阻塞队列中未完成的任务,阻塞队列中的元素也就不会再执行了
这里可能网友会有疑问了,怎么判断是空闲线程的???
我们可以看到interruptIdleWorkers方法,向线程发出中断信号前,需要获得tryLock()获取独占锁,才能执行t.interrupt()方法,我们再返过头来看runWorker()方法
可以看到如果getTask()返回不为null,则会执行任务,则会拿到独占锁,那么对于正在执行任务的线程是无法发出中断信息号的。除非任务执行完,释放锁。那么interruptIdleWorkers中的w.tryLock()才能拿到锁,发出中断信号
再次进入到while的判断条件里面去。我们再来看getTask()方法第一个地方返回null的地方
返回null的条件需要两个,一个是shutdown状态,还有一个是队列为空,没有任务(暂时不考虑stop状态)。如果队列中还存在任务,则会调用poll或者take获取任务
这里网友可能会想到线程不是已经是中断状态了吗?那么调用poll和take方法时,应该会一直抛出InterruptedException异常才对。如果对AQS比较熟悉的话,应该可以想到不会出现这种情况,我们再来看poll和task调用的方法
虽然是会抛出中断异常,但是线程中断状态会被重置,也就是下次线程不是中断状态,可以继续获取任务执行,直到队列为空。队列为空,线程池状态又为shutdown状态,最后会返回null。
如果线程本身在获取任务是阻塞住了,然后shutdown发出中断信号,抛出中断异常,再次进入到第一个地方判断条件,那么逻辑就跟3.1类似,会判断队列是否是空的,如果是非空的,那么继续会调用poll和take获取任务,首次去获取的时候线程的中断状态也会被重置,下次就能正常的获取任务,直到队列为空,线程池状态为shutdown状态,返回null
总的来说,ThreadPoolExecutor回收线程都是等getTask()获取不到任务,返回null时,调用processWorkerExit方法从Set集合中remove掉线程,getTask()返回null又分为2两种场景:
-
线程正常执行完任务,并且已经等到超过keepAliveTime时间,大于核心线程数,那么会返回null,结束外层的runWorker中的while循环
-
当调用shutdown()方法,会将线程池状态置为shutdown,并且需要等待正在执行的任务执行完,阻塞队列中的任务执行完才能返回null
在工作中,一直认为编程代码不是最重要的,重要的是在工作中所养成的编程思维。
已在知识星球更新源码解析如下:
最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。
提供近 3W 行代码的 SpringBoot 示例,以及超 6W 行代码的电商微服务项目。
文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)