前言
這是一個真實的面試題根穷。
前幾天一個朋友在群里分享了他剛剛面試候選者時問的問題:"線程池如何按照core姜骡、max、queue的執(zhí)行循序去執(zhí)行缠诅?"溶浴。
我們都知道線程池中代碼執(zhí)行順序是:corePool->workQueue->maxPool,源碼我都看過管引,你現(xiàn)在問題讓我改源碼士败??
一時間群里炸開了鍋褥伴,小伙伴們紛紛打聽他所在的公司谅将,然后拉黑避坑。(手動狗頭重慢,大家一起調(diào)侃?(?????)?)
關于線程池他一共問了這么幾個問題:
- 線程池如何按照core饥臂、max、queue的順序去執(zhí)行似踱?
- 子線程拋出的異常隅熙,主線程能感知到么稽煤?
- 線程池發(fā)生了異常改怎樣處理?
全是一些有意思的問題囚戚,我之前也寫過一篇很詳細的圖文教程:【萬字圖文-原創(chuàng)】 | 學會Java中的線程池酵熙,這一篇也許就夠了! 驰坊,不了解的小伙伴可以再回顧下~
但是針對這幾個問題匾二,可能大家一時間也有點懵。今天的文章我們以源碼為基礎來分析下該如何回答這三個問題拳芙。(之前沒閱讀過源碼也沒關系察藐,所有的分析都會貼出源碼及圖解)
線程池如何按照core、max舟扎、queue的順序執(zhí)行分飞?
問題思考
對于這個問題,很多小伙伴肯定會疑惑:"別人源碼中寫好的執(zhí)行流程你為啥要改睹限?這面試官腦子有病吧......"
這里來思考一下現(xiàn)實工作場景中是否有這種需求浸须?之前也看到過一份簡歷也寫到過這個問題:
一個線程池執(zhí)行的任務屬于IO
密集型,CPU
大多屬于閑置狀態(tài)邦泄,系統(tǒng)資源未充分利用。如果一瞬間來了大量請求裂垦,如果線程池數(shù)量大于coreSize
時顺囊,多余的請求都會放入到等待隊列中。等待著corePool
中的線程執(zhí)行完成后再來執(zhí)行等待隊列中的任務蕉拢。
試想一下特碳,這種場景我們該如何優(yōu)化?
我們可以修改線程池的執(zhí)行順序為corePool->maxPool->workQueue晕换。 這樣就能夠充分利用CPU
資源午乓,提交的任務會被優(yōu)先執(zhí)行。當線程池中線程數(shù)量大于maxSize
時才會將任務放入等待隊列中闸准。
你就說巧不巧益愈?面試官的這個問題顯然是經(jīng)過認真思考來提問的,這是一個很有意思的溫恩提夷家,下面就一起看看如何解決吧蒸其。
線程池運行流程
我們都知道線程池執(zhí)行流程是先corePool
再workQueue
,最后才是maxPool
的一個執(zhí)行流程库快。
線程池核心參數(shù)
在回顧下ThreadPoolExecutor.execute()
源碼前我們先回顧下線程池中的幾個重要參數(shù):
我們來看下這幾個參數(shù)的定義:
corePoolSize
: 線程池中核心線程數(shù)量
maximumPoolSize
: 線程池中最大線程數(shù)量
keepAliveTime
: 非核心的空閑線程等待新任務的時間
unit
: 時間單位摸袁。配合allowCoreThreadTimeOut
也會清理核心線程池中的線程。
workQueue
: 基于Blocking
的任務隊列义屏,最好選用有界隊列靠汁,指定隊列長度
threadFactory
: 線程工廠蜂大,最好自定義線程工廠,可以自定義每個線程的名稱
handler
: 拒絕策略蝶怔,默認是AbortPolicy
ThreadPoolExecutor.execute()源碼分析
我們可以看下execute()
如下:
接著來分析下執(zhí)行過程:
- 第一步:
workerCountOf(c)
時間計算當前線程池中線程的個數(shù)奶浦,當線程個數(shù)小于核心線程數(shù) - 第二步:線程池線程數(shù)量大于核心線程數(shù),此時提交的任務會放入
workQueue
中添谊,使用offer()
進行操作 - 第三步:
workQueue.offer()
執(zhí)行失敗财喳,新提交的任務會直接執(zhí)行,addWorker()
會判斷如果當前線程池數(shù)量大于最大線程數(shù)斩狱,則執(zhí)行拒絕策略
好了耳高,到了這里我們都已經(jīng)很清楚了,關鍵在于第二步和第三步如何交換順序執(zhí)行呢所踊?
解決思路
仔細想一想泌枪,如果修改workQueue.offer()
的實現(xiàn)不就可以達到目的了?我們先來畫圖來看一下:
現(xiàn)在的問題就在于秕岛,如果當前線程池中coreSize < workCount < maxSize
時碌燕,一定會先執(zhí)行offer()
操作。
我們?nèi)绻薷?code>offer的實現(xiàn)是否可以完成執(zhí)行順序的更換呢继薛?這里也是畫圖來展示一下:
Dubbo中EagerThreadPool解決方案
湊巧Dubbo
中也有類似的實現(xiàn)修壕,在Dubbo
的EagerThreadPool
自定義了一個BlockingQueue
,在offer()
方法中遏考,如果當前線程池數(shù)量小于最大線程池時慈鸠,直接返回false
,這里就達到了調(diào)節(jié)線程池執(zhí)行順序的目的灌具。
看到這里一切都真相大白了青团,解決思路以及方案都很簡單,學會了沒有咖楣?
這個問題背后還隱藏了一些場景的優(yōu)化督笆、源碼的擴展等等知識,果然是一個值得思考的好問題诱贿。
子線程拋出的異常娃肿,主線程能感知到么?
問題思考
這個問題其實也很容易回答瘪松,也僅僅是一個面試題而已咸作,實際工作中子線程的異常不應該由主線程來捕獲。
針對這個問題宵睦,希望大家清楚的是: 我們要明確線程代碼的邊界记罚,異步化過程中,子線程拋出的異常應該由子線程自己去處理壳嚎,而不是需要主線程感知來協(xié)助處理桐智。
解決方案
解決方案很簡單歉甚,在虛擬機中奥秆,當一個線程如果沒有顯式處理異常而拋出時會將該異常事件報告給該線程對象的 java.lang.Thread.UncaughtExceptionHandler
進行處理刘绣,如果線程沒有設置 UncaughtExceptionHandler
摄欲,則默認會把異常棧信息輸出到終端而使程序直接崩潰。
所以如果我們想在線程意外崩潰時做一些處理就可以通過實現(xiàn) UncaughtExceptionHandler
來滿足需求刊驴。
我們使用線程池設置ThreadFactory
時可以指定UncaughtExceptionHandler
姿搜,這樣就可以捕獲到子線程拋出的異常了。
代碼示例
具體代碼如下:
/**
* 測試子線程異常問題
*
* @author wangmeng
* @date 2020/6/13 18:08
*/
public class ThreadPoolExceptionTest {
public static void main(String[] args) throws InterruptedException {
MyHandler myHandler = new MyHandler();
ExecutorService execute = new ThreadPoolExecutor(10, 10,
0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), new ThreadFactoryBuilder().setUncaughtExceptionHandler(myHandler).build());
TimeUnit.SECONDS.sleep(5);
for (int i = 0; i < 10; i++) {
execute.execute(new MyRunner());
}
}
private static class MyRunner implements Runnable {
@Override
public void run() {
int count = 0;
while (true) {
count++;
System.out.println("我要開始生產(chǎn)Bug了============");
if (count == 10) {
System.out.println(1 / 0);
}
if (count == 20) {
System.out.println("這里是不會執(zhí)行到的==========");
break;
}
}
}
}
}
class MyHandler implements Thread.UncaughtExceptionHandler {
private final static Logger LOGGER = LoggerFactory.getLogger(MyHandler.class);
@Override
public void uncaughtException(Thread t, Throwable e) {
LOGGER.error("threadId = {}, threadName = {}, ex = {}", t.getId(), t.getName(), e.getMessage());
}
}
執(zhí)行結果:
UncaughtExceptionHandler 解析
我們來看下Thread
中的內(nèi)部接口UncaughtExceptionHandler
:
public class Thread {
......
/**
* 當一個線程因未捕獲的異常而即將終止時虛擬機將使用 Thread.getUncaughtExceptionHandler()
* 獲取已經(jīng)設置的 UncaughtExceptionHandler 實例捆憎,并通過調(diào)用其 uncaughtException(...) 方
* 法而傳遞相關異常信息舅柜。
* 如果一個線程沒有明確設置其 UncaughtExceptionHandler,則將其 ThreadGroup 對象作為其
* handler躲惰,如果 ThreadGroup 對象對異常沒有什么特殊的要求致份,則 ThreadGroup 會將調(diào)用轉發(fā)給
* 默認的未捕獲異常處理器(即 Thread 類中定義的靜態(tài)未捕獲異常處理器對象)。
*
* @see #setDefaultUncaughtExceptionHandler
* @see #setUncaughtExceptionHandler
* @see ThreadGroup#uncaughtException
*/
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* 未捕獲異常崩潰時回調(diào)此方法
*/
void uncaughtException(Thread t, Throwable e);
}
/**
* 靜態(tài)方法础拨,用于設置一個默認的全局異常處理器氮块。
*/
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
defaultUncaughtExceptionHandler = eh;
}
/**
* 針對某個 Thread 對象的方法,用于對特定的線程進行未捕獲的異常處理诡宗。
*/
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
checkAccess();
uncaughtExceptionHandler = eh;
}
/**
* 當 Thread 崩潰時會調(diào)用該方法獲取當前線程的 handler滔蝉,獲取不到就會調(diào)用 group(handler 類型)。
* group 是 Thread 類的 ThreadGroup 類型屬性塔沃,在 Thread 構造中實例化锰提。
*/
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
/**
* 線程全局默認 handler。
*/
public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() {
return defaultUncaughtExceptionHandler;
}
......
}
部分內(nèi)容參考自:https://mp.weixin.qq.com/s/ghnNQnpou6-NemhFjpl4Jg
線程池發(fā)生了異常改怎樣處理芳悲?
線程池中線程運行過程中出現(xiàn)了異常該怎樣處理呢?線程池提交任務有兩種方式边坤,分別是execute()
和submit()
名扛,這里會依次說明。
ThreadPoolExecutor.runWorker()實現(xiàn)
不管是使用execute()
還是submit()
提交任務茧痒,最終都會執(zhí)行到ThreadPoolExecutor.runWorker()
肮韧,我們來看下源碼(源碼基于JDK1.8):
我們看到在執(zhí)行task.run()
時,出現(xiàn)異常會直接向上拋出旺订,這里處理的最好的方式就是在我們業(yè)務代碼中使用try...catch()
來捕獲異常弄企。
FutureTask.run()實現(xiàn)
如果我們使用submit()
來提交任務,在ThreadPoolExecutor.runWorker()
方法執(zhí)行時最終會調(diào)用到FutureTask.run()
方法里面去区拳,不清楚的小伙伴也可以看下我之前的文章:
線程池續(xù):你必須要知道的線程池submit()實現(xiàn)原理之FutureTask拘领!
這里可以看到,如果業(yè)務代碼拋出異常后樱调,會被catch
捕獲到约素,然后調(diào)用setExeception()
方法:
可以看到其實類似于直接吞掉了届良,當我們調(diào)用get()
方法的時候異常信息會包裝到FutureTask內(nèi)部的變量outcome中,我們也會獲取到對應的異常信息圣猎。
在ThreadPoolExecutor.runWorker()
最后finally
中有一個afterExecute()
鉤子方法士葫,如果我們重寫了afterExecute()
方法,就可以獲取到子線程拋出的具體異常信息Throwable
了送悔。
結論
對于線程池慢显、包括線程的異常處理推薦以下方式:
- 直接使用
try/catch
,這個也是最推薦的方式 - 在我們構造線程池的時候欠啤,重寫
uncaughtException()
方法荚藻,上面示例代碼也有提到:
public class ThreadPoolExceptionTest {
public static void main(String[] args) throws InterruptedException {
MyHandler myHandler = new MyHandler();
ExecutorService execute = new ThreadPoolExecutor(10, 10,
0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), new ThreadFactoryBuilder().setUncaughtExceptionHandler(myHandler).build());
TimeUnit.SECONDS.sleep(5);
for (int i = 0; i < 10; i++) {
execute.execute(new MyRunner());
}
}
}
class MyHandler implements Thread.UncaughtExceptionHandler {
private final static Logger LOGGER = LoggerFactory.getLogger(MyHandler.class);
@Override
public void uncaughtException(Thread t, Throwable e) {
LOGGER.error("threadId = {}, threadName = {}, ex = {}", t.getId(), t.getName(), e.getMessage());
}
}
3 直接重寫afterExecute()
方法,感知異常細節(jié)
總結
這篇文章到這里就結束了跪妥,不知道小伙伴們有沒有一些感悟或收獲鞋喇?
通過這幾個面試問題,我也深刻的感受到學習知識要多思考眉撵,看源碼的過程中要多設置一些場景侦香,這樣才會收獲更多。