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

4次阅读

共计 4772 个字符,预计需要花费 12 分钟才能阅读完成。

作者: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 开发手册(嵩山版)》最新公布,速速下载!

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

正文完
 0