关于java:面试官线程池多余的线程是如何回收的

作者:kingsleylam\
链接:https://cnblogs.com/kingsleyl…

最近浏览了JDK线程池ThreadPoolExecutor的源码,对线程池执行工作的流程有了大体理解,实际上这个流程也非常通俗易懂,就不再赘述了,他人写的比我好多了。

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

上面以JDK1.8为例进行剖析

1. runWorker(Worker w)

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

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

取工作的起源有两个,一个是firstTask,这个是工作线程第一次跑的时候执行的工作,最多只能执行一次,前面得从getTask()办法里取工作。看来,getTask()是要害,在不思考异样的场景下,返回null,就示意退出循环,完结线程。下一步,就得看看,什么状况下getTask()会返回null。

(篇幅无限,分段截取,省略两头执行工作的步骤)

2、getTask() 返回null

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

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

第二种状况,工作线程数曾经大于最大线程数或当前工作线程已超时,且,还有其余工作线程或工作队列为空。这点比拟难了解,总之先记住,前面会用。

上面以条件1和条件2别离指代这两种状况的判断条件。

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()之后,会向所有的闲暇工作线程发送中断信号。

最终传入false,调用上面这个办法。

能够看出,在收回中断信号前,会判断是否曾经中断,以及要取得工作线程的独占锁。

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

所以咱们只有看getTask()外面怎么应答中断异样的就能够了。

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

3.2.1 工作已全副实现,线程在阻塞期待。

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

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

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.

阶段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) 的办法。

能够看到,在外面除了workers.remove(w) 移除线,还调用了tryTerminate()。

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

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

答案依然是,想多了……

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

也就是说,退出的线程不能相互中断,我从汇合中退出后,中断了你,你不能中断我,因为我曾经退出汇合,你只能中断他人。那么,即便有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(),向任意闲暇线程收回中断信号。所有被阻塞的线程,最终都会被一个个唤醒,回收。

近期热文举荐:

1.1,000+ 道 Java面试题及答案整顿(2021最新版)

2.别在再满屏的 if/ else 了,试试策略模式,真香!!

3.卧槽!Java 中的 xx ≠ null 是什么新语法?

4.Spring Boot 2.6 正式公布,一大波新个性。。

5.《Java开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞+转发哦!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理