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

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

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

按照這個思路,我們可以很容易的實現(xiàn)一個簡單版線程池,想必看了下面這個代碼實現(xiàn),對線程池的核心原理就會了然于心。
首先線程池內(nèi)需要定義兩個成員變量,分別是阻塞隊列和線程列表,然后自定義線程使它的任務(wù)就是不斷的從阻塞隊列中拿任務(wù)然后執(zhí)行。
@Slf4j
public class YesThreadPool {
BlockingQueue<Runnable> taskQueue; //存放任務(wù)的阻塞隊列
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ù)隊列里面塞任務(wù)
public void execute(Runnable task) throws InterruptedException {
taskQueue.put(task);
}
class YesThread extends Thread { //自定義一個線程
public YesThread(String name) {
super(name);
}
@Override
public void run() {
while (true) { //死循環(huán)
Runnable task = null;
try {
task = taskQueue.take(); //不斷從任務(wù)隊列獲取任務(wù)
} catch (InterruptedException e) {
logger.error("記錄點東西.....", e);
}
task.run(); //執(zhí)行
}
}
}
}
一個簡單版線程池就完成了,簡單吧!
再寫個 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() + " 公眾號:yes的練級攻略");
});
} catch (InterruptedException e) {
logger.error("記錄點東西.....", e);
}
});
}
運行結(jié)果如下:

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

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

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

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

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

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

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

注釋里面也寫的很清楚,我再畫個圖

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

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

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

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

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

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

我們來看下這個方法,其實邏輯很簡單,把這個線程廢了,然后新建一個線程替換之。

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

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

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

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

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

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

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

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

沒啥花頭,調(diào)用的 interruptIdleWorkers 之前都分析過了。
我再貼一下之前寫過的線程池設(shè)計面試題吧。
如果要讓你設(shè)計一個線程池,你要怎么設(shè)計?
這種設(shè)計類問題還是一樣,先說下理解,表明你是知道這個東西的用處和原理的,然后開始 BB。基本上就是按照現(xiàn)有的設(shè)計來說,再添加一些個人見解。
線程池講白了就是存儲線程的一個容器,池內(nèi)保存之前建立過的線程來重復(fù)執(zhí)行任務(wù),減少創(chuàng)建和銷毀線程的開銷,提高任務(wù)的響應(yīng)速度,并便于線程的管理。
我個人覺得如果要設(shè)計一個線程池的話得考慮池內(nèi)工作線程的管理、任務(wù)編排執(zhí)行、線程池超負(fù)荷處理方案、監(jiān)控。
初始化線程數(shù)、核心線程數(shù)、最大線程池都暴露出來可配置,包括超過核心線程數(shù)的線程空閑消亡配置。
任務(wù)的存儲結(jié)構(gòu)可配置,可以是無界隊列也可以是有界隊列,也可以根據(jù)配置分多個隊列來分配不同優(yōu)先級的任務(wù),也可以采用 stealing 的機制來提高線程的利用率。
再提供配置來表明此線程池是 IO 密集還是 CPU 密集型來改變?nèi)蝿?wù)的執(zhí)行策略。
超負(fù)荷的方案可以有多種,包括丟棄任務(wù)、拒絕任務(wù)并拋出異常、丟棄最舊的任務(wù)或自定義等等。
線程池埋好點暴露出用于監(jiān)控的接口,如已處理任務(wù)數(shù)、待處理任務(wù)數(shù)、正在運行的線程數(shù)、拒絕的任務(wù)數(shù)等等信息。
我覺得基本上這樣答就差不多了,等著面試官的追問就好。
注意不需要跟面試官解釋什么叫核心線程數(shù)之類的,都懂的沒必要。
當(dāng)然這種開放型問題還是仁者見仁智者見智,我這個不是標(biāo)準(zhǔn)答案,僅供參考。
關(guān)于線程池的一點碎碎念
線程池的好處我們都知道了,但是不是任何時刻都上線程池的,我看過一些奇怪的代碼,就是為了用線程池而用線程池...
還有需要根據(jù)不同的業(yè)務(wù)劃分不同的線程池,不然會存在一些耗時的業(yè)務(wù)影響了另一個業(yè)務(wù)導(dǎo)致這個業(yè)務(wù)崩了,然后都崩了的情況,所以要做好線程池隔離。
最后
好了,有關(guān)線程池的知識點和一些常見的一些面試題應(yīng)該都涉及到了吧,如果還有別的啥角度刁鉆的面試題,歡迎留言提出,咱們一起研究研究。
相信看了這篇文章之后,關(guān)于線程池出去面試可以開始吹了!
如果覺得文章不錯,來個點贊和在看唄!
— 【 THE END 】— 本公眾號全部博文已整理成一個目錄,請在公眾號里回復(fù)「m」獲取! 最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點“在看”,關(guān)注公眾號并回復(fù) PDF 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。
謝謝支持喲 (*^__^*)
