如圖兩道面試題,順便深入線程池,并連環(huán)17問(wèn)
這兩面試題是基友朋友最近去面滴滴遇到的,今天就借著這兩面試真題來(lái)深入一波線程池吧,這篇文章力求把線程池核心點(diǎn)和常問(wèn)的面試點(diǎn)一網(wǎng)打盡,當(dāng)然個(gè)人能力有限,可能會(huì)有遺漏,歡迎留言補(bǔ)充!
先把問(wèn)題列出來(lái),如果你都答得出來(lái),那沒(méi)必要看下去:
為什么會(huì)有線程池? 簡(jiǎn)單手寫(xiě)一個(gè)線程池? 為什么要把任務(wù)先放在任務(wù)隊(duì)列里面,而不是把線程先拉滿到最大線程數(shù)? 線程池如何動(dòng)態(tài)修改核心線程數(shù)和最大線程數(shù)? 如果你是 JDK 設(shè)計(jì)者,如何設(shè)計(jì)? 如果要讓你設(shè)計(jì)一個(gè)線程池,你要怎么設(shè)計(jì)? 你是如何理解核心線程的? 你是怎么理解 KeepAliveTime 的? 那 workQueue 有什么用? 你是如何理解拒絕策略的? 你說(shuō)你看過(guò)源碼,那你肯定知道線程池里的 ctl 是干嘛的咯? 你知道線程池有幾種狀態(tài)嗎? 你知道線程池的狀態(tài)是如何變遷的嗎? 如何修改原生線程池,使得可以先拉滿線程數(shù)再入任務(wù)隊(duì)列排隊(duì)? Tomcat 中的定制化線程池實(shí)現(xiàn) 如果線程池中的線程在執(zhí)行任務(wù)的時(shí)候,拋異常了,會(huì)怎么樣? 原生線程池的核心線程一定伴隨著任務(wù)慢慢創(chuàng)建的嗎? 線程池的核心線程在空閑的時(shí)候一定不會(huì)被回收嗎?
接得住嗎?話不多說(shuō),發(fā)車!
為什么會(huì)有線程池?
想要深入理解線程池的原理得先知道為什么需要線程池。
首先你要明白,線程是一個(gè)重資源,JVM 中的線程與操作系統(tǒng)的線程是一對(duì)一的關(guān)系,所以在 JVM 中每創(chuàng)建一個(gè)線程就需要調(diào)用操作系統(tǒng)提供的 API 創(chuàng)建線程,賦予資源,并且銷毀線程同樣也需要系統(tǒng)調(diào)用。
而系統(tǒng)調(diào)用就意味著上下文切換等開(kāi)銷,并且線程也是需要占用內(nèi)存的,而內(nèi)存也是珍貴的資源。
因此線程的創(chuàng)建和銷毀是一個(gè)重操作,并且線程本身也占用資源。
然后你還需要知道,線程數(shù)并不是越多越好。
我們都知道線程是 CPU 調(diào)度的最小單位,在單核時(shí)代,如果是純運(yùn)算的操作是不需要多線程的,一個(gè)線程一直執(zhí)行運(yùn)算即可。但如果這個(gè)線程正在等待 I/O 操作,此時(shí) CPU 就處于空閑狀態(tài),這就浪費(fèi)了 CPU 的算力,因此有了多線程,在某線程等待 I/O 等操作的時(shí)候,另一個(gè)線程頂上,充分利用 CPU,提高處理效率。

此時(shí)的多線程主要是為了提高 CPU 的利用率而提出。
而隨著 CPU 的發(fā)展,核心數(shù)越來(lái)越多,能同時(shí)運(yùn)行的線程數(shù)也提升了,此時(shí)的多線程不僅是為了提高單核 CPU 的利用率,也是為了充分利用多個(gè)核心。
至此想必應(yīng)該明白了為什么會(huì)有多線程,無(wú)非就是為了充分利用 CPU 空閑的時(shí)間,一刻也不想讓他停下來(lái)。
但 CPU 的核心數(shù)有限,同時(shí)能運(yùn)行的線程數(shù)有限,所以需要根據(jù)調(diào)度算法切換執(zhí)行的線程,而線程的切換需要開(kāi)銷,比如替換寄存器的內(nèi)容、高速緩存的失效等等。
如果線程數(shù)太多,切換的頻率就變高,可能使得多線程帶來(lái)的好處抵不過(guò)線程切換帶來(lái)的開(kāi)銷,得不償失。
因此線程的數(shù)量需要得以控制,結(jié)合上述的描述可知,線程的數(shù)量與 CPU 核心數(shù)和 I/O 等待時(shí)長(zhǎng)息息相關(guān)。
小結(jié)一下:
Java中線程與操作系統(tǒng)線程是一比一的關(guān)系。 線程的創(chuàng)建和銷毀是一個(gè)“較重”的操作。 多線程的主要是為了提高 CPU 的利用率。 線程的切換有開(kāi)銷,線程數(shù)的多少需要結(jié)合 CPU核心數(shù)與 I/O 等待占比。
綜上我們知道了線程的這些特性,所以說(shuō)它不是一個(gè)可以“隨意拿捏”的東西,我們需要重視它,好好規(guī)劃和管理它,充分利用硬件的能力,從而提升程序執(zhí)行效率,所以線程池應(yīng)運(yùn)而生。
什么是線程池?
那我們要如何管理好線程呢?
因?yàn)榫€程數(shù)太少無(wú)法充分利用 CPU ,太多的話由于上下文切換的消耗又得不償失,所以我們需要評(píng)估系統(tǒng)所要承載的并發(fā)量和所執(zhí)行任務(wù)的特性,得出大致需要多少個(gè)線程數(shù)才能充分利用 CPU,因此需要控制線程數(shù)量。
又因?yàn)榫€程的創(chuàng)建和銷毀是一個(gè)“重”操作,所以我們需要避免線程頻繁地創(chuàng)建與銷毀,因此我們需要緩存一批線程,讓它們時(shí)刻準(zhǔn)備著執(zhí)行任務(wù)。
目標(biāo)已經(jīng)很清晰了,弄一個(gè)池子,里面存放約定數(shù)量的線程,這就是線程池,一種池化技術(shù)。
熟悉對(duì)象池、連接池的朋友肯定對(duì)池化技術(shù)不陌生,一般池化技術(shù)的使用方式是從池子里拿出資源,然后使用,用完了之后歸還。
但是線程池的實(shí)現(xiàn)不太一樣,不是說(shuō)我們從線程池里面拿一個(gè)線程來(lái)執(zhí)行任務(wù),等任務(wù)執(zhí)行完了之后再歸還線程,你可以想一下這樣做是否合理。
線程池的常見(jiàn)實(shí)現(xiàn)更像是一個(gè)黑盒存在,我們?cè)O(shè)置好線程池的大小之后,直接往線程池里面丟任務(wù),然后就不管了。

剝開(kāi)來(lái)看,線程池其實(shí)是一個(gè)典型的生產(chǎn)者-消費(fèi)者模式。
線程池內(nèi)部會(huì)有一個(gè)隊(duì)列來(lái)存儲(chǔ)我們提交的任務(wù),而內(nèi)部線程不斷地從隊(duì)列中索取任務(wù)來(lái)執(zhí)行,這就是線程池最原始的執(zhí)行機(jī)制。

按照這個(gè)思路,我們可以很容易的實(shí)現(xiàn)一個(gè)簡(jiǎn)單版線程池,想必看了下面這個(gè)代碼實(shí)現(xiàn),對(duì)線程池的核心原理就會(huì)了然于心。
首先線程池內(nèi)需要定義兩個(gè)成員變量,分別是阻塞隊(duì)列和線程列表,然后自定義線程使它的任務(wù)就是不斷的從阻塞隊(duì)列中拿任務(wù)然后執(zhí)行。
@Slf4j
public class YesThreadPool {
BlockingQueue<Runnable> taskQueue; //存放任務(wù)的阻塞隊(duì)列
List<YesThread> threads; //線程列表
YesThreadPool(BlockingQueue<Runnable> taskQueue, int threadSize) {
this.taskQueue = taskQueue;
threads = new ArrayList<>(threadSize);
// 初始化線程,并定義名稱
IntStream.rangeClosed(1, threadSize).forEach((i)-> {
YesThread thread = new YesThread("yes-task-thread-" + i);
thread.start();
threads.add(thread);
});
}
//提交任務(wù)只是往任務(wù)隊(duì)列里面塞任務(wù)
public void execute(Runnable task) throws InterruptedException {
taskQueue.put(task);
}
class YesThread extends Thread { //自定義一個(gè)線程
public YesThread(String name) {
super(name);
}
@Override
public void run() {
while (true) { //死循環(huán)
Runnable task = null;
try {
task = taskQueue.take(); //不斷從任務(wù)隊(duì)列獲取任務(wù)
} catch (InterruptedException e) {
logger.error("記錄點(diǎn)東西.....", e);
}
task.run(); //執(zhí)行
}
}
}
}
一個(gè)簡(jiǎn)單版線程池就完成了,簡(jiǎn)單吧!
再寫(xiě)個(gè) main 方法用一用,絲滑,非常絲滑。
public static void main(String[] args) {
YesThreadPool pool = new YesThreadPool(new LinkedBlockingQueue<>(10), 3);
IntStream.rangeClosed(1, 5).forEach((i)-> {
try {
pool.execute(()-> {
System.out.println(Thread.currentThread().getName() + " 公眾號(hào):yes的練級(jí)攻略");
});
} catch (InterruptedException e) {
logger.error("記錄點(diǎn)東西.....", e);
}
});
}
運(yùn)行結(jié)果如下:

下次面試官讓你手寫(xiě)線程池,直接上這個(gè)簡(jiǎn)單版,然后他會(huì)開(kāi)始讓你優(yōu)化,比如什么線程一開(kāi)始都 start 了不好,想懶加載,然后xxxx...最終其實(shí)就是想往李老爺實(shí)現(xiàn)的 ThreadPoolExecutor 上面靠。
那就來(lái)嘛。
ThreadPoolExecutor 剖析
這玩意就是常被問(wèn)的線程池的實(shí)現(xiàn)類了,先來(lái)看下構(gòu)造函數(shù):

核心原理其實(shí)和咱們上面實(shí)現(xiàn)的差不多,只是生產(chǎn)級(jí)別的那肯定是要考慮的更多,接下來(lái)我們就來(lái)看看此線程池的工作原理。
先來(lái)一張圖:

簡(jiǎn)單來(lái)說(shuō)線程池把任務(wù)的提交和任務(wù)的執(zhí)行剝離開(kāi)來(lái),當(dāng)一個(gè)任務(wù)被提交到線程池之后:
如果此時(shí)線程數(shù)小于核心線程數(shù),那么就會(huì)新起一個(gè)線程來(lái)執(zhí)行當(dāng)前的任務(wù)。 如果此時(shí)線程數(shù)大于核心線程數(shù),那么就會(huì)將任務(wù)塞入阻塞隊(duì)列中,等待被執(zhí)行。 如果阻塞隊(duì)列滿了,并且此時(shí)線程數(shù)小于最大線程數(shù),那么會(huì)創(chuàng)建新線程來(lái)執(zhí)行當(dāng)前任務(wù)。 如果阻塞隊(duì)列滿了,并且此時(shí)線程數(shù)大于最大線程數(shù),那么會(huì)采取拒絕策略。
以上就是任務(wù)提交給線程池后各種狀況匯總,一個(gè)很容易出現(xiàn)理解錯(cuò)誤的地方就是當(dāng)線程數(shù)達(dá)到核心數(shù)的時(shí)候,任務(wù)是先入隊(duì),而不是先創(chuàng)建最大線程數(shù)。
從上述可知,線程池里的線程不是一開(kāi)始就直接拉滿的,是根據(jù)任務(wù)量開(kāi)始慢慢增多的,這就算一種懶加載,到用的時(shí)候再創(chuàng)建線程,節(jié)省資源。
來(lái)先吃我?guī)讍?wèn)。
此時(shí)線程數(shù)小于核心線程數(shù),并且線程都處于空閑狀態(tài),現(xiàn)提交一個(gè)任務(wù),是新起一個(gè)線程還是給之前創(chuàng)建的線程?
李老是這樣說(shuō)的:If fewer than corePoolSize threads are running, try to start a new thread with the given command as its first task.
我覺(jué)得把 threads are running 去了,更合理一些,此時(shí)線程池會(huì)新起一個(gè)線程來(lái)執(zhí)行這個(gè)新任務(wù),不管老線程是否空閑。
你是如何理解核心線程的 ?
從上一個(gè)問(wèn)題可以看出,線程池雖說(shuō)默認(rèn)是懶創(chuàng)建線程,但是它實(shí)際是想要快速擁有核心線程數(shù)的線程。核心線程指的是線程池承載日常任務(wù)的中堅(jiān)力量,也就是說(shuō)本質(zhì)上線程池是需要這么些數(shù)量的線程來(lái)處理任務(wù)的,所以在懶中又急著創(chuàng)建它。
而最大線程數(shù)其實(shí)是為了應(yīng)付突發(fā)狀況。
舉個(gè)裝修的例子,正常情況下施工隊(duì)只要 5 個(gè)人去干活,這 5 人其實(shí)就是核心線程,但是由于工頭接的活太多了,導(dǎo)致 5 個(gè)人在約定工期內(nèi)干不完,所以工頭又去找了 2 個(gè)人來(lái)一起干,所以 5 是核心線程數(shù),7 是最大線程數(shù)。
平時(shí)就是 5 個(gè)人干活,特別忙的時(shí)候就找 7 個(gè),等閑下來(lái)就會(huì)把多余的 2 個(gè)辭了。
看到這里你可能會(huì)覺(jué)得核心線程在線程池里面會(huì)有特殊標(biāo)記?
并沒(méi)有,不論是核心還是非核心線程,在線程池里面都是一視同仁,當(dāng)淘汰的時(shí)候不會(huì)管是哪些線程,反正留下核心線程數(shù)個(gè)線程即可,下文會(huì)作詳解。
你是怎么理解 KeepAliveTime 的?
這就是上面提到的,線程池其實(shí)想要的只是核心線程數(shù)個(gè)線程,但是又預(yù)留了一些數(shù)量來(lái)預(yù)防突發(fā)狀況,當(dāng)突發(fā)狀況過(guò)去之后,線程池希望只維持核心線程數(shù)的線程,所以就弄了個(gè) KeepAliveTime,當(dāng)線程數(shù)大于核心數(shù)之后,如果線程空閑了一段時(shí)間(KeepAliveTime),就回收線程,直到數(shù)量與核心數(shù)持平。
那 workQueue 有什么用?
緩存任務(wù)供線程獲取,這里要注意限制工作隊(duì)列的大小。隊(duì)列長(zhǎng)了,堆積的任務(wù)就多,堆積的任務(wù)多,后面任務(wù)等待的時(shí)長(zhǎng)就長(zhǎng)。
想想你點(diǎn)擊一個(gè)按鈕是一直轉(zhuǎn)圈等半天沒(méi)反應(yīng)舒服,還是直接報(bào)錯(cuò)舒服,所以有時(shí)心是好的,想盡量完成提交的任務(wù),但是用戶體驗(yàn)不如直接拒絕。更有可能由于允許囤積的任務(wù)過(guò)多,導(dǎo)致資源耗盡而系統(tǒng)崩潰。
所以工作隊(duì)列起到一個(gè)緩沖作用,具體隊(duì)列長(zhǎng)度需要結(jié)合線程數(shù),任務(wù)的執(zhí)行時(shí)長(zhǎng),能承受的等待時(shí)間等。
你是如何理解拒絕策略的?
線程數(shù)總有拉滿的一天,工作隊(duì)列也是一樣,如果兩者都滿了,此時(shí)的提交任務(wù)就需要拒絕,默認(rèn)實(shí)現(xiàn)是 AbortPolicy 直接拋出異常。

剩下的拒絕策略有直接丟棄任務(wù)一聲不吭的、讓提交任務(wù)的線程自己運(yùn)行的、淘汰老的未執(zhí)行的任務(wù)而空出位置的,具體用哪個(gè)策略,根據(jù)場(chǎng)景選擇。當(dāng)然也可以自定義拒絕策略,實(shí)現(xiàn) RejectedExecutionHandler 這個(gè)接口即可。
所以線程池盡可能只維護(hù)核心數(shù)量的線程,提供任務(wù)隊(duì)列暫存任務(wù),并提供拒絕策略來(lái)應(yīng)對(duì)過(guò)載的任務(wù)。
這里還有個(gè)細(xì)節(jié),如果線程數(shù)已經(jīng)達(dá)到核心線程數(shù),那么新增加的任務(wù)只會(huì)往任務(wù)隊(duì)列里面塞,不會(huì)直接給予某個(gè)線程,如果任務(wù)隊(duì)列也滿了,新增最大線程數(shù)的線程時(shí),任務(wù)是可以直接給予新建的線程執(zhí)行的,而不是入隊(duì)。
感覺(jué)已經(jīng)會(huì)了?那再來(lái)看幾道面試題:
你說(shuō)你看過(guò)源碼,那你肯定知道線程池里的 ctl 是干嘛的咯?

其實(shí)看下注釋就很清楚了,ctl 是一個(gè)涵蓋了兩個(gè)概念的原子整數(shù)類,它將工作線程數(shù)和線程池狀態(tài)結(jié)合在一起維護(hù),低 29 位存放 workerCount,高 3 位存放 runState。

其實(shí)并發(fā)包中有很多實(shí)現(xiàn)都是一個(gè)字段存多個(gè)值的,比如讀寫(xiě)鎖的高 16 位存放讀鎖,低 16 位存放寫(xiě)鎖,這種一個(gè)字段存放多個(gè)值可以更容易的維護(hù)多個(gè)值之間的一致性,也算是極簡(jiǎn)主義。
你知道線程池有幾種狀態(tài)嗎?

注解說(shuō)的很明白,我再翻譯一下:
RUNNING:能接受新任務(wù),并處理阻塞隊(duì)列中的任務(wù) SHUTDOWN:不接受新任務(wù),但是可以處理阻塞隊(duì)列中的任務(wù) STOP:不接受新任務(wù),并且不處理阻塞隊(duì)列中的任務(wù),并且還打斷正在運(yùn)行任務(wù)的線程,就是直接撂擔(dān)子不干了! TIDYING:所有任務(wù)都終止,并且工作線程也為0,處于關(guān)閉之前的狀態(tài) TERMINATED:已關(guān)閉。
你知道線程池的狀態(tài)是如何變遷的嗎?

注釋里面也寫(xiě)的很清楚,我再畫(huà)個(gè)圖

為什么要把任務(wù)先放在任務(wù)隊(duì)列里面,而不是把線程先拉滿到最大線程數(shù)?
我說(shuō)下我的個(gè)人理解。
其實(shí)經(jīng)過(guò)上面的分析可以得知,線程池本意只是讓核心數(shù)量的線程工作著,不論是 core 的取名,還是 keepalive 的設(shè)定,所以你可以直接把 core 的數(shù)量設(shè)為你想要線程池工作的線程數(shù),而任務(wù)隊(duì)列起到一個(gè)緩沖的作用。最大線程數(shù)這個(gè)參數(shù)更像是無(wú)奈之舉,在最壞的情況下做最后的努力,去新建線程去幫助消化任務(wù)。
所以我個(gè)人覺(jué)得沒(méi)有為什么,就是這樣設(shè)計(jì)的,并且這樣的設(shè)定挺合理。
當(dāng)然如果你想要扯一扯 CPU 密集和 I/O 密集,那可以扯一扯。
原生版線程池的實(shí)現(xiàn)可以認(rèn)為是偏向 CPU 密集的,也就是當(dāng)任務(wù)過(guò)多的時(shí)候不是先去創(chuàng)建更多的線程,而是先緩存任務(wù),讓核心線程去消化,從上面的分析我們可以知道,當(dāng)處理 CPU 密集型任務(wù)的時(shí),線程太多反而會(huì)由于線程頻繁切換的開(kāi)銷而得不償失,所以優(yōu)先堆積任務(wù)而不是創(chuàng)建新的線程。
而像 Tomcat 這種業(yè)務(wù)場(chǎng)景,大部分情況下是需要大量 I/O 處理的情況就做了一些定制,修改了原生線程池的實(shí)現(xiàn),使得在隊(duì)列沒(méi)滿的時(shí)候,可以創(chuàng)建線程至最大線程數(shù)。
如何修改原生線程池,使得可以先拉滿線程數(shù)再入任務(wù)隊(duì)列排隊(duì)?
如果了解線程池的原理,很輕松的就知道關(guān)鍵點(diǎn)在哪,就是隊(duì)列的 offer 方法。

execute 方法想必大家都不陌生,就是給線程池提交任務(wù)的方法。在這個(gè)方法中可以看到只要在 offer 方法內(nèi)部判斷此時(shí)線程數(shù)還小于最大線程數(shù)的時(shí)候返回 false,即可走下面 else if 中 addWorker (新增線程)的邏輯,如果數(shù)量已經(jīng)達(dá)到最大線程數(shù),直接入隊(duì)即可。
詳細(xì)的我們可以看看 Tomcat 中是如何定制線程的。
Tomcat 中的定制化線程池實(shí)現(xiàn)
public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {}
可以看到先繼承了 JUC 的線程池,然后我們重點(diǎn)關(guān)注一下 execute 這個(gè)方法

這里可以看到,Tomcat 維護(hù)了一個(gè) submittedCount 變量,這個(gè)變量的含義是統(tǒng)計(jì)已經(jīng)提交的但是還未完成的任務(wù)數(shù)量(記住這個(gè)變量,很關(guān)鍵),所以只要提交一個(gè)任務(wù),這個(gè)數(shù)就加一,并且捕獲了拒絕異常,再次嘗試將任務(wù)入隊(duì),這個(gè)操作其實(shí)是為了盡可能的挽救回一些任務(wù),因?yàn)檫@么點(diǎn)時(shí)間差可能已經(jīng)執(zhí)行完很多任務(wù),隊(duì)列騰出了空位,這樣就不需要丟棄任務(wù)。
然后我們?cè)賮?lái)看下代碼里出現(xiàn)的 TaskQueue,這個(gè)就是上面提到的定制關(guān)鍵點(diǎn)了。
public class TaskQueue extends LinkedBlockingQueue<Runnable> {
private transient volatile ThreadPoolExecutor parent = null;
........
}
可以看到這個(gè)任務(wù)隊(duì)列繼承了 LinkedBlockingQueue,并且有個(gè) ThreadPoolExecutor 類型的成員變量 parent ,我們?cè)賮?lái)看下 offer 方法的實(shí)現(xiàn),這里就是修改原來(lái)線程池任務(wù)提交與線程創(chuàng)建邏輯的核心了。

從上面的邏輯可以看出是有機(jī)會(huì)在隊(duì)列還未滿的時(shí)候,先創(chuàng)建線程至最大線程數(shù)的!
再補(bǔ)充一下,如果對(duì)直接返回 false 就能創(chuàng)建線程感到疑惑的話,往上翻一翻,上面貼了原生線程池 execute 的邏輯。
然后上面的代碼其實(shí)只看到 submittedCount 的增加,正常的減少在 afterExecute 里實(shí)現(xiàn)了。

而這個(gè) afterExecute 在任務(wù)執(zhí)行完畢之后就會(huì)調(diào)用,與之對(duì)應(yīng)的還有個(gè) beforeExecute,在任務(wù)執(zhí)行之前調(diào)用。

至此,想必 Tomcat 中的定制化線程池的邏輯已經(jīng)明白了。
如果線程池中的線程在執(zhí)行任務(wù)的時(shí)候,拋異常了,會(huì)怎么樣?
嘿嘿,細(xì)心的同學(xué)想必已經(jīng)瞄到了上面的代碼,task.run() 被 try catch finally包裹,異常被扔到了 afterExecute 中,并且也繼續(xù)被拋了出來(lái)。
而這一層外面,還有個(gè)try finally,所以異常的拋出打破了 while 循環(huán),最終會(huì)執(zhí)行 `processWorkerExit方法

我們來(lái)看下這個(gè)方法,其實(shí)邏輯很簡(jiǎn)單,把這個(gè)線程廢了,然后新建一個(gè)線程替換之。

移除了引用等于銷毀了,這事兒 GC 會(huì)做的。
所以如果一個(gè)任務(wù)執(zhí)行一半就拋出異常,并且你沒(méi)有自行處理這個(gè)異常,那么這個(gè)任務(wù)就這樣戛然而止了,后面也不會(huì)有線程繼續(xù)執(zhí)行剩下的邏輯,所以要自行捕獲和處理業(yè)務(wù)異常。
addWorker 的邏輯就不分析了,就是新建一個(gè)線程,然后塞到 workers 里面,然后調(diào)用 start() 讓它跑起來(lái)。
原生線程池的核心線程一定伴隨著任務(wù)慢慢創(chuàng)建的嗎?
并不是,線程池提供了兩個(gè)方法:
prestartCoreThread:?jiǎn)?dòng)一個(gè)核心線程 prestartAllCoreThreads :?jiǎn)?dòng)所有核心線程
不要小看這個(gè)預(yù)創(chuàng)建方法,預(yù)熱很重要,不然剛重啟的一些服務(wù)有時(shí)是頂不住瞬時(shí)請(qǐng)求的,就立馬崩了,所以有預(yù)熱線程、緩存等等操作。
線程池的核心線程在空閑的時(shí)候一定不會(huì)被回收嗎?
有個(gè) allowCoreThreadTimeOut 方法,把它設(shè)置為 true ,則所有線程都會(huì)超時(shí),不會(huì)有核心數(shù)那條線的存在。
具體是會(huì)調(diào)用 interruptIdleWorkers 這個(gè)方法。

這里需要講一下的是 w.tryLock() 這個(gè)方法,有些人可能會(huì)奇怪,Worker 怎么還能 lock。
Worker 是屬于工作線程的封裝類,它不僅實(shí)現(xiàn)了 Runnable 接口,還繼承了 AQS。

之所以要繼承 AQS 就是為了用上 lock 的狀態(tài),執(zhí)行任務(wù)的時(shí)候上鎖,任務(wù)執(zhí)行完了之后解鎖,這樣執(zhí)行關(guān)閉線程池等操作的時(shí)候可以通過(guò) tryLock 來(lái)判斷此時(shí)線程是否在干活,如果 tryLock 成功說(shuō)明此時(shí)線程是空閑的,可以安全的回收。
與interruptIdleWorkers 對(duì)應(yīng)的還有一個(gè) interruptWorkers 方法,從名字就能看出差別,不空閑的 worker 也直接給打斷了。
根據(jù)這兩個(gè)方法,又可以扯到 shutdown 和 shutdownNow,就是關(guān)閉線程池的方法,一個(gè)是安全的關(guān)閉線程池,會(huì)等待任務(wù)都執(zhí)行完畢,一個(gè)是粗暴的直接咔嚓了所有線程,管你在不在運(yùn)行,兩個(gè)方法分別調(diào)用的就是 interruptIdleWorkers() 和 interruptWorkers() 來(lái)中斷線程。

這又可以引申出一個(gè)問(wèn)題,shutdownNow 了之后還在任務(wù)隊(duì)列中的任務(wù)咋辦?眼尖的小伙伴應(yīng)該已經(jīng)看到了,線程池還算負(fù)責(zé),把未執(zhí)行的任務(wù)拖拽到了一個(gè)列表中然后返回,至于怎么處理,就交給調(diào)用者了!
詳細(xì)就是上面的 drainQueue 方法。

這里可能又會(huì)有同學(xué)有疑問(wèn),都 drainTo 了,為什么還要判斷一下隊(duì)列是否為空,然后進(jìn)行循環(huán)?
那是因?yàn)槿绻?duì)列是 DelayQueue 或任何其他類型的隊(duì)列,其中 poll 或 drainTo 可能無(wú)法刪除某些元素,所以需要遍歷,逐個(gè)刪除它們。
回到最開(kāi)始的面試題
線程池如何動(dòng)態(tài)修改核心線程數(shù)和最大線程數(shù)?
其實(shí)之所以會(huì)有這樣的需求是因?yàn)榫€程數(shù)是真的不好配置。
你可能會(huì)在網(wǎng)上或者書(shū)上看到很多配置公式,比如:
CPU 密集型的話,核心線程數(shù)設(shè)置為 CPU核數(shù)+1 I/O 密集型的話,核心線程數(shù)設(shè)置為 2*CPU核數(shù)
比如:
線程數(shù)=CPU核數(shù) *(1+線程等待時(shí)間 / 線程時(shí)間運(yùn)行時(shí)間)
這個(gè)比上面的更貼合與業(yè)務(wù),還有一些理想的公式就不列了。就這個(gè)公式而言,這個(gè)線程等待時(shí)間就很難測(cè),拿 Tomcat 線程池為例,每個(gè)請(qǐng)求的等待時(shí)間能知道?不同的請(qǐng)求不同的業(yè)務(wù),就算相同的業(yè)務(wù),不同的用戶數(shù)據(jù)量也不同,等待時(shí)間也不同。
所以說(shuō)線程數(shù)真的很難通過(guò)一個(gè)公式一勞永逸,線程數(shù)的設(shè)定是一個(gè)迭代的過(guò)程,需要壓測(cè)適時(shí)調(diào)整,以上的公式做個(gè)初始值開(kāi)始調(diào)試是 ok 的。
再者,流量的突發(fā)性也是無(wú)法判斷的,舉個(gè)例子 1 秒內(nèi)一共有 1000 個(gè)請(qǐng)求量,但是如果這 1000 個(gè)請(qǐng)求量都是在第一毫秒內(nèi)瞬時(shí)進(jìn)來(lái)的呢?
這就很需要線程池的動(dòng)態(tài)性,也是這個(gè)上面這個(gè)面試題的需求來(lái)源。
原生的線程池核心我們大致都過(guò)了一遍,不過(guò)這幾個(gè)方法一直沒(méi)提到,先來(lái)看看這幾個(gè)方法:

我就不一一翻譯了,大致可以看出線程池其實(shí)已經(jīng)給予方法暴露出內(nèi)部的一些狀態(tài),例如正在執(zhí)行的線程數(shù)、已完成的任務(wù)數(shù)、隊(duì)列中的任務(wù)數(shù)等等。
當(dāng)然你可以想要更多的數(shù)據(jù)監(jiān)控都簡(jiǎn)單的,像 Tomcat 那種繼承線程池之后自己加唄,動(dòng)態(tài)調(diào)整的第一步監(jiān)控就這樣搞定了!定時(shí)拉取這些數(shù)據(jù),然后搞個(gè)看板,再結(jié)合郵件、短信、釘釘?shù)葓?bào)警方式,我們可以很容易的監(jiān)控線程池的狀態(tài)!
接著就是動(dòng)態(tài)修改線程池配置了。

可以看到線程池已經(jīng)提供了諸多修改方法來(lái)更改線程池的配置,所以李老都已經(jīng)考慮到啦!
同樣,也可以繼承線程池增加一些方法來(lái)修改,看具體的業(yè)務(wù)場(chǎng)景了。同樣搞個(gè)頁(yè)面,然后給予負(fù)責(zé)人員配置修改即可。
所以原生線程池已經(jīng)提供修改配置的方法,也對(duì)外暴露出線程池內(nèi)部執(zhí)行情況,所以只要我們實(shí)時(shí)監(jiān)控情況,調(diào)用對(duì)應(yīng)的 set 方法,即可動(dòng)態(tài)修改線程池對(duì)應(yīng)配置。
回答面試題的時(shí)候一定要提監(jiān)控,顯得你是有的放矢的。
如果你是 JDK 設(shè)計(jì)者,如何設(shè)計(jì)?
其實(shí)我覺(jué)得這個(gè)是緊接著上一題問(wèn)的,應(yīng)該算是同一個(gè)問(wèn)題。
而且 JDK 設(shè)計(jì)者已經(jīng)設(shè)計(jì)好了呀?所以怎么說(shuō)我也不清楚,不過(guò)我們可以說(shuō)一說(shuō)具體的實(shí)現(xiàn)邏輯唄。
先來(lái)看下設(shè)置核心線程數(shù)的方法:

隨著注釋看下來(lái)應(yīng)該沒(méi)什么問(wèn)題,就是那個(gè) k 值我說(shuō)一下,李老覺(jué)得核心線程數(shù)是配置了,但是此時(shí)線程池內(nèi)部是否需要這么多線程是不確定的,那么就按工作隊(duì)列里面的任務(wù)數(shù)來(lái),直接按任務(wù)數(shù)立刻新增線程,當(dāng)任務(wù)隊(duì)列為空了之后就終止新增。
這其實(shí)和李老設(shè)計(jì)的默認(rèn)核心線程數(shù)增加策略是一致的,都是懶創(chuàng)建線程。
再看看設(shè)置最大線程數(shù)的方法:

沒(méi)啥花頭,調(diào)用的 interruptIdleWorkers 之前都分析過(guò)了。
我再貼一下之前寫(xiě)過(guò)的線程池設(shè)計(jì)面試題吧。
如果要讓你設(shè)計(jì)一個(gè)線程池,你要怎么設(shè)計(jì)?
這種設(shè)計(jì)類問(wèn)題還是一樣,先說(shuō)下理解,表明你是知道這個(gè)東西的用處和原理的,然后開(kāi)始 BB?;旧暇褪前凑宅F(xiàn)有的設(shè)計(jì)來(lái)說(shuō),再添加一些個(gè)人見(jiàn)解。
線程池講白了就是存儲(chǔ)線程的一個(gè)容器,池內(nèi)保存之前建立過(guò)的線程來(lái)重復(fù)執(zhí)行任務(wù),減少創(chuàng)建和銷毀線程的開(kāi)銷,提高任務(wù)的響應(yīng)速度,并便于線程的管理。
我個(gè)人覺(jué)得如果要設(shè)計(jì)一個(gè)線程池的話得考慮池內(nèi)工作線程的管理、任務(wù)編排執(zhí)行、線程池超負(fù)荷處理方案、監(jiān)控。
初始化線程數(shù)、核心線程數(shù)、最大線程池都暴露出來(lái)可配置,包括超過(guò)核心線程數(shù)的線程空閑消亡配置。
任務(wù)的存儲(chǔ)結(jié)構(gòu)可配置,可以是無(wú)界隊(duì)列也可以是有界隊(duì)列,也可以根據(jù)配置分多個(gè)隊(duì)列來(lái)分配不同優(yōu)先級(jí)的任務(wù),也可以采用 stealing 的機(jī)制來(lái)提高線程的利用率。
再提供配置來(lái)表明此線程池是 IO 密集還是 CPU 密集型來(lái)改變?nèi)蝿?wù)的執(zhí)行策略。
超負(fù)荷的方案可以有多種,包括丟棄任務(wù)、拒絕任務(wù)并拋出異常、丟棄最舊的任務(wù)或自定義等等。
線程池埋好點(diǎn)暴露出用于監(jiān)控的接口,如已處理任務(wù)數(shù)、待處理任務(wù)數(shù)、正在運(yùn)行的線程數(shù)、拒絕的任務(wù)數(shù)等等信息。
我覺(jué)得基本上這樣答就差不多了,等著面試官的追問(wèn)就好。
注意不需要跟面試官解釋什么叫核心線程數(shù)之類的,都懂的沒(méi)必要。
當(dāng)然這種開(kāi)放型問(wèn)題還是仁者見(jiàn)仁智者見(jiàn)智,我這個(gè)不是標(biāo)準(zhǔn)答案,僅供參考。
關(guān)于線程池的一點(diǎn)碎碎念
線程池的好處我們都知道了,但是不是任何時(shí)刻都上線程池的,我看過(guò)一些奇怪的代碼,就是為了用線程池而用線程池...
還有需要根據(jù)不同的業(yè)務(wù)劃分不同的線程池,不然會(huì)存在一些耗時(shí)的業(yè)務(wù)影響了另一個(gè)業(yè)務(wù)導(dǎo)致這個(gè)業(yè)務(wù)崩了,然后都崩了的情況,所以要做好線程池隔離。
最后
好了,有關(guān)線程池的知識(shí)點(diǎn)和一些常見(jiàn)的一些面試題應(yīng)該都涉及到了吧,如果還有別的啥角度刁鉆的面試題,歡迎留言提出,咱們一起研究研究。
相信看了這篇文章之后,關(guān)于線程池出去面試可以開(kāi)始吹了!
如果覺(jué)得文章不錯(cuò),來(lái)個(gè)點(diǎn)贊和在看唄!
