面試官一個(gè)線程池問(wèn)題把我問(wèn)懵逼了。

大家好,我是躍哥。想必大家在面試的時(shí)候,經(jīng)常被問(wèn)到線程池的問(wèn)題,那今天躍哥就借助于 why 哥的文章,和大家來(lái)好好聊聊線程池。
這是why的第 98 篇原創(chuàng)文章
你好呀,我是why哥。
前幾天,有個(gè)朋友在微信上找我。他問(wèn):why哥,在嗎?
我說(shuō):發(fā)生腎么事了?
他啪的一下就提了一個(gè)問(wèn)題啊,很快。
我大意了,隨意瞅了一眼,這題不是很簡(jiǎn)單嗎?
結(jié)果沒(méi)想到里面還隱藏著一篇文章。
故事,得從這個(gè)問(wèn)題說(shuō)起:

上面的圖中的線程池配置是這樣的:
ExecutorService executorService = new ThreadPoolExecutor(40, 80, 1, TimeUnit.MINUTES,
new LinkedBlockingQueue<>(100),
new DefaultThreadFactory("test"),
new ThreadPoolExecutor.DiscardPolicy());
上面這個(gè)線程池里面的參數(shù)、執(zhí)行流程啥的我就不再解釋了。
畢竟我曾經(jīng)在《一人血書(shū),想讓why哥講一下這道面試題。》這篇文章里面發(fā)過(guò)毒誓的,再說(shuō)就是小王吧了:

上面的這個(gè)問(wèn)題其實(shí)就是一個(gè)非常簡(jiǎn)單的八股文問(wèn)題:
非核心線程在什么時(shí)候被回收?
如果經(jīng)過(guò) keepAliveTime 時(shí)間后,超過(guò)核心線程數(shù)的線程還沒(méi)有接受到新的任務(wù),就會(huì)被回收。
標(biāo)準(zhǔn)答案,完全沒(méi)毛病。
那么我現(xiàn)在帶入一個(gè)簡(jiǎn)單的場(chǎng)景,為了簡(jiǎn)單直觀,我們把線程池相關(guān)的參數(shù)調(diào)整一下:
ExecutorService executorService = new ThreadPoolExecutor(2, 3, 30, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2),
new DefaultThreadFactory("test"),
new ThreadPoolExecutor.DiscardPolicy());
那么問(wèn)題來(lái)了:
這個(gè)線程最多能容納的任務(wù)是不是 5 個(gè)? 假設(shè)任務(wù)需要執(zhí)行 1 秒鐘,那么我直接循環(huán)里面提交 5 個(gè)任務(wù)到線程池,肯定是在 1 秒鐘之內(nèi)提交完成,那么當(dāng)前線程池的活躍線程是不是就是 3 個(gè)? 如果接下來(lái)的 30 秒,沒(méi)有任務(wù)提交過(guò)來(lái)。那么 30 秒之后,當(dāng)前線程池的活躍線程是不是就是 2 個(gè)?
上面這三個(gè)問(wèn)題的答案都是肯定的,如果你搞不明白為什么,那么我建議你先趕緊去補(bǔ)充一下線程池相關(guān)的知識(shí)點(diǎn),下面的內(nèi)容你強(qiáng)行看下去肯定是一臉懵逼的。
接下來(lái)的問(wèn)題是這樣的,請(qǐng)聽(tīng)題:
如果當(dāng)前線程池的活躍線程是 3 個(gè)(2 個(gè)核心線程+ 1 個(gè)非核心線程),但是它們各自的任務(wù)都執(zhí)行完成了。然后我每隔 3 秒往線程池里面扔一個(gè)耗時(shí) 1 秒的任務(wù)。那么 30 秒之后,活躍線程數(shù)是多少?
先說(shuō)答案:還是 3 個(gè)。
從我個(gè)人正常的思維,是這樣的:核心線程是空閑的,每隔 3 秒扔一個(gè)耗時(shí) 1 秒的任務(wù)過(guò)來(lái),所以?xún)H需要一個(gè)核心線程就完全處理的過(guò)來(lái)。
那么,30 秒內(nèi),超過(guò)核心線程的那一個(gè)線程一直處于等待狀態(tài),所以 30 秒之后,就被回收了。
但是上面僅僅是我的主觀認(rèn)為,而實(shí)際情況呢?
30 秒之后,超過(guò)核心線程的線程并不會(huì)被回收,活躍線程還是 3 個(gè)。
到這里,如果你知道是 3 個(gè),且知道為什么是 3 個(gè),即了解為什么非核心線程并沒(méi)有被回收,那么接下里的內(nèi)容應(yīng)該就是你已經(jīng)掌握的了。
可以不看,拉到最后,點(diǎn)個(gè)贊,去忙自己的事情吧。
如果你不知道,可以接著看,了解一下為什么是 3 個(gè)。
雖然我相信沒(méi)有面試官會(huì)問(wèn)這樣的問(wèn)題,但是對(duì)于你去理解線程池,是有幫助的。
先上 Demo
基于我前面說(shuō)的這個(gè)場(chǎng)景,碼出代碼如下:
public class ThreadTest {
@Test
public void test() throws InterruptedException {
ThreadPoolExecutor executorService = new ThreadPoolExecutor(2, 3, 30, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2), new DefaultThreadFactory("test"),
new ThreadPoolExecutor.DiscardPolicy());
//每隔兩秒打印線程池的信息
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println("=====================================thread-pool-info:" + new Date() + "=====================================");
System.out.println("CorePoolSize:" + executorService.getCorePoolSize());
System.out.println("PoolSize:" + executorService.getPoolSize());
System.out.println("ActiveCount:" + executorService.getActiveCount());
System.out.println("KeepAliveTime:" + executorService.getKeepAliveTime(TimeUnit.SECONDS));
System.out.println("QueueSize:" + executorService.getQueue().size());
}, 0, 2, TimeUnit.SECONDS);
try {
//同時(shí)提交5個(gè)任務(wù),模擬達(dá)到最大線程數(shù)
for (int i = 0; i < 5; i++) {
executorService.execute(new Task());
}
} catch (Exception e) {
e.printStackTrace();
}
//休眠10秒,打印日志,觀察線程池狀態(tài)
Thread.sleep(10000);
//每隔3秒提交一個(gè)任務(wù)
while (true) {
Thread.sleep(3000);
executorService.submit(new Task());
}
}
static class Task implements Runnable {
@Override
public void run(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "-執(zhí)行任務(wù)");
}
}
}
這份代碼也是提問(wèn)的哥們給我的,我做了細(xì)微的調(diào)整,你直接粘出去就能跑起來(lái)。
show code,no bb。這才是相互探討的正確姿勢(shì)。
這個(gè)程序的運(yùn)行結(jié)果是這樣的:

一共五個(gè)任務(wù),線程池的運(yùn)行情況是什么樣的呢?
先看標(biāo)號(hào)為 ① 的地方:
三個(gè)線程都在執(zhí)行任務(wù),然后 2 號(hào)線程和 1 號(hào)線程率先完成了任務(wù),接著把隊(duì)列里面的兩個(gè)任務(wù)拿出來(lái)執(zhí)行(標(biāo)號(hào)為 ② 的地方)。
按照程序,接下來(lái),每隔 3 秒就有一個(gè)耗時(shí) 1 秒的任務(wù)過(guò)來(lái)。而此時(shí)線程池里面的三個(gè)活躍線程都是空閑狀態(tài)。
那么問(wèn)題就來(lái)了:
該選擇哪個(gè)線程來(lái)執(zhí)行這個(gè)任務(wù)呢?是隨機(jī)選一個(gè)嗎?
雖然接下來(lái)的程序還沒(méi)有執(zhí)行,但是基于前面的截圖,我現(xiàn)在就可以告訴你,接下來(lái)的任務(wù),線程執(zhí)行順序?yàn)椋?/p>
Thread[test-1-3,5,main]-執(zhí)行任務(wù) Thread[test-1-2,5,main]-執(zhí)行任務(wù) Thread[test-1-1,5,main]-執(zhí)行任務(wù) Thread[test-1-3,5,main]-執(zhí)行任務(wù) Thread[test-1-2,5,main]-執(zhí)行任務(wù) Thread[test-1-1,5,main]-執(zhí)行任務(wù) ......
即在我們的案例中,雖然線程都是空閑的,但是當(dāng)任務(wù)來(lái)的時(shí)候不是隨機(jī)調(diào)用的,而是輪詢(xún)。
由于是輪詢(xún),每三秒執(zhí)行一次,所以非核心線程的空閑時(shí)間最多也就是 9 秒,不會(huì)超過(guò) 30 秒,所以一直不會(huì)被回收。
基于這個(gè) Demo,我們就從表象上回答了,為什么活躍線程數(shù)一直為 3。
這個(gè)地方就和我的認(rèn)知有點(diǎn)出入了,于是我稍微的研究了一下為什么是輪詢(xún)。
為什么是輪詢(xún)?
我們通過(guò) Demo 驗(yàn)證了在上面場(chǎng)景中線程執(zhí)行順序?yàn)檩喸?xún)。
那么為什么呢?
這只是通過(guò)日志得出的表象呀,內(nèi)部原理呢?對(duì)應(yīng)的代碼呢?
這一小節(jié)帶大家看一下到底是怎么回事。
首先我看到這個(gè)表象的時(shí)候我就猜測(cè):這三個(gè)線程肯定是在某個(gè)地方被某個(gè)隊(duì)列存起來(lái)了,基于此,才能實(shí)現(xiàn)輪詢(xún)調(diào)用。
所以,我一直在找這個(gè)隊(duì)列,一直沒(méi)有找到對(duì)應(yīng)的代碼,我還有點(diǎn)著急了。想著不會(huì)是在操作系統(tǒng)層面控制的吧?
后來(lái)我冷靜下來(lái),覺(jué)得不太可能。于是電光火石之間,我想到了,要不先 Dump 一下線程,看看它們都在干啥:

Dump 之后,這玩意我眼熟啊,AQS 的等待隊(duì)列啊。
先說(shuō)明一下:由于本文只是帶著你去找答案在源碼的什么地方,不對(duì)源碼進(jìn)行解讀。所以我默認(rèn)你是對(duì) AQS 是有一定的了解的。
接著根據(jù)堆棧信息,我們可以定位到這里的源碼:
java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#awaitNanos

看到這里的時(shí)候,我才一下恍然大悟了起來(lái)。
害,是自己想的太多了。
說(shuō)穿了,這其實(shí)就是個(gè)生產(chǎn)者-消費(fèi)者的問(wèn)題啊。
三個(gè)線程就是三個(gè)消費(fèi)者,現(xiàn)在沒(méi)有任務(wù)需要處理,它們就等著生產(chǎn)者生產(chǎn)任務(wù),然后通知它們準(zhǔn)備消費(fèi)。
可以看到 addConditionWaiter 方法其實(shí)就是在操作我們要找的那個(gè)隊(duì)列,學(xué)名叫做等待隊(duì)列。
Debug 一下,看看隊(duì)列里面的情況:

巧了嘛,這不是。順序剛好是:
Thread[test-1-3,5,main] Thread[test-1-2,5,main] Thread[test-1-1,5,main]
消費(fèi)者這邊我們大概摸清楚了,接著去看看生產(chǎn)者。
java.util.concurrent.ThreadPoolExecutor#execute

線程池是在這里把任務(wù)放到隊(duì)列里面去的。
而這個(gè)方法里面的源碼是這樣的:

其中signalNotEmpty() 最終會(huì)走到 doSignal 方法,而該方法里面會(huì)調(diào)用 transferForSignal 方法。
這個(gè)方法里面會(huì)調(diào)用 LockSupport.unpark(node.thred) 方法,喚醒線程:

而喚醒的順序,就是等待隊(duì)列里面的順序:

所以,現(xiàn)在你知道當(dāng)一個(gè)任務(wù)來(lái)了之后,這個(gè)任務(wù)該由線程池里面的哪個(gè)線程執(zhí)行,這個(gè)不是隨機(jī)的,也不是隨便來(lái)的。
是講究一個(gè)順序的。
什么順序呢?
Condition 里面的等待隊(duì)列里面的順序。
什么,你不太懂 Condition?
那還不趕緊去學(xué)?
本來(lái)我是想寫(xiě)一下的,后來(lái)發(fā)現(xiàn)《Java并發(fā)編程的藝術(shù)》一書(shū)中的 5.6.2 小節(jié)已經(jīng)寫(xiě)的挺清楚了,圖文并茂。這部分內(nèi)容其實(shí)也是面試的時(shí)候的高頻考點(diǎn),所以自己去看看就好了。

我就把我寫(xiě)的這部分內(nèi)容刪除了,先就不贅述了吧。
哦,你不想看書(shū),就想等著我給你講呢?
先欠著,欠著。
偷個(gè)懶,文章寫(xiě)太長(zhǎng)了也沒(méi)人看。

非核心線程怎么回收?
還是上面的例子,假設(shè)非核心線程就空閑了超過(guò) 30 秒,那么它是怎么被回收的呢?
這個(gè)也是一個(gè)比較熱門(mén)的面試題。
這題沒(méi)有什么高深的地方,答案就藏在源碼的這個(gè)地方:
java.util.concurrent.ThreadPoolExecutor#getTask

當(dāng) timed 參數(shù)為 true 的時(shí)候,會(huì)執(zhí)行 workQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS) 方法。
而 timed 什么時(shí)候?yàn)?true 呢?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
allowCoreThreadTimeOut 默認(rèn)為 false。

所以,就是看 wc > corePoolSize 條件,wc 是活躍線程數(shù)。此時(shí)活躍線程數(shù)為 3 ,大于核心線程數(shù) 2。
因此 timed 為 true。
也就是說(shuō),當(dāng)前 workQueue 為空的時(shí)候,現(xiàn)在三個(gè)線程都阻塞 workQueue.poll 方法中。
而當(dāng)指定時(shí)間后,workQueue 還是為空,則返回為 null。
于是在 1077 行把 timeOut 修改為 true。
進(jìn)入一下次循環(huán),返回 null。
最終會(huì)執(zhí)行到這個(gè)方法:
java.util.concurrent.ThreadPoolExecutor#processWorkerExit

而這個(gè)方法里面會(huì)執(zhí)行 remove 的操作。
于是線程就被回收了。
所以當(dāng)超過(guò)指定時(shí)間后,線程會(huì)被回收。
那么被回收的這個(gè)線程是核心線程還是非核心線程呢?
不知道。
因?yàn)樵诰€程池里面,核心線程和非核心線程僅僅是一個(gè)概念而已,其實(shí)拿著一個(gè)線程,我們并不能知道它是核心線程還是非核心線程。
這個(gè)地方就是一個(gè)證明,因?yàn)楫?dāng)工作線程多余核心線程數(shù)之后,所有的線程都在 poll,也就是說(shuō)所有的線程都有可能被回收:

另外一個(gè)強(qiáng)有力的證明就是 addWorker 這里:

core 參數(shù)僅僅是控制取 corePoolSize 還是 maximumPoolSize。
所以,這個(gè)問(wèn)題你說(shuō)怎么回答:

JDK 區(qū)分的方式就是不區(qū)分。
那么我們可以知道嗎?
可以,比如通過(guò)觀察日志,前面的案例中,我就知道這兩個(gè)是核心線程,因?yàn)樗鼈冏钕葎?chuàng)建:
Thread[test-1-1,5,main]-執(zhí)行任務(wù) Thread[test-1-2,5,main]-執(zhí)行任務(wù)
在程序里面怎么知道呢?
這個(gè)就比較難了,其實(shí)我覺(jué)得這個(gè)信息并不重要吧?
什么,你加錢(qián)?
加錢(qián),加錢(qián)可以實(shí)現(xiàn)。

自己擴(kuò)展一下線程池嘛,給線程池里面的線程打個(gè)標(biāo)還不是一件很簡(jiǎn)單的事情嗎?
只是你想想,你區(qū)分這玩意干啥,有沒(méi)有可落地的需求?
畢竟,脫離需求談實(shí)現(xiàn)。都是耍流氓。
荒腔走板

周末的時(shí)候收到了之前在網(wǎng)上買(mǎi)的一個(gè)頭戴式藍(lán)牙耳機(jī),放在家里用。
本來(lái)我是想多選選、多看看、多糾結(jié)一下的。
但是當(dāng)我刷購(gòu)物 APP 的時(shí)候,突然魅族的這款耳機(jī)映入了我的眼睛里面。
在這之前,其實(shí)我完全不知道這款耳機(jī)的。然后點(diǎn)進(jìn)去簡(jiǎn)單看了一下介紹,甚至都沒(méi)看評(píng)論,就直接下單了。
不為別的,只是因?yàn)槲以?jīng)也是魅族的忠實(shí)用戶(hù)。
但是下單的時(shí)候我就在想:這應(yīng)該是我最后一次為魅族充值了吧。
我用的第一款智能手機(jī)就是從同學(xué)那里買(mǎi)來(lái)的魅族的 M8,快 10 年過(guò)去了,我至今都還記得當(dāng)時(shí)第一眼看到它的驚艷。
一款手機(jī),怎么能做的這么漂亮呢?
提前 M8,了解魅族的朋友、了解這款手機(jī)的朋友都會(huì)由衷的說(shuō)一句:國(guó)貨之光。
由于 M8 給我?guī)?lái)的良好體驗(yàn),后來(lái)的 10 年間,我每次換手機(jī)都是魅族。
我真的喜歡魅族、喜歡 flyme 系統(tǒng)、喜歡 mBack。
直到后面,止步于 Pro 7,黃章的“出山之作”。我當(dāng)時(shí)剛好換手機(jī),果斷的買(mǎi)了,用了。之后,我決定不再為魅族充值了。
在我心里,魅族創(chuàng)始人黃章,才是第一個(gè)打造情懷,買(mǎi)情懷的人。我這句話不是貶低,而是贊揚(yáng),我曾經(jīng)就愿意為這個(gè)男人的情懷買(mǎi)單。
但是,不管怎么樣,我還是希望黃章早日做出自己的“夢(mèng)想機(jī)”,魅族重回巔峰。
畢竟,我看到耳機(jī)快遞里面魅族科技的卡片時(shí),內(nèi)心還是有一絲波瀾的。
畢竟,我曾經(jīng)也是魅友。
說(shuō)到這個(gè)耳機(jī),是真不錯(cuò)。
我寫(xiě)文章的時(shí)候就帶著這個(gè)耳機(jī),不愧是 MP3 發(fā)家的魅族:
高音準(zhǔn),中音甜,低音勁,總之一句話,就是通透!
最后說(shuō)一句
好了,看到了這里安排個(gè)“一鍵三連”(轉(zhuǎn)發(fā)、在看、點(diǎn)贊)吧,周更很累的,需要一點(diǎn)正反饋。

才疏學(xué)淺,難免會(huì)有紕漏,如果你發(fā)現(xiàn)了錯(cuò)誤的地方,可以在后臺(tái)提出來(lái),我對(duì)其加以修改。

0、重磅!兩萬(wàn)字長(zhǎng)文總結(jié),梳理 Java 入門(mén)進(jìn)階哪些事(推薦收藏)

