前言
這是一個真實的面試題,
前幾天一個朋友在群里分享了他剛剛面試候選者時問的問題:"執行緒池如何按照core、max、queue的執行循序去執行?",
我們都知道執行緒池中代碼執行順序是:corePool->workQueue->maxPool,原始碼我都看過,你現在問題讓我改原始碼??
一時間群里炸開了鍋,小伙伴們紛紛打聽他所在的公司,然后拉黑避坑,(手動狗頭,大家一起調侃?(?????)?)
關于執行緒池他一共問了這么幾個問題:
- 執行緒池如何按照core、max、queue的順序去執行?
- 子執行緒拋出的例外,主執行緒能感知到么?
- 執行緒池發生了例外改怎樣處理?
全是一些有意思的問題,我之前也寫過一篇很詳細的圖文教程:【萬字圖文-原創】 | 學會Java中的執行緒池,這一篇也許就夠了! ,不了解的小伙伴可以再回顧下~
但是針對這幾個問題,可能大家一時間也有點懵,今天的文章我們以原始碼為基礎來分析下該如何回答這三個問題,(之前沒閱讀過原始碼也沒關系,所有的分析都會貼出原始碼及圖解)
執行緒池如何按照core、max、queue的順序執行?
問題思考
對于這個問題,很多小伙伴肯定會疑惑:"別人原始碼中寫好的執行流程你為啥要改?這面試官腦子有病吧......"
這里來思考一下現實作業場景中是否有這種需求?之前也看到過一份簡歷也寫到過這個問題:

一個執行緒池執行的任務屬于IO密集型,CPU大多屬于閑置狀態,系統資源未充分利用,如果一瞬間來了大量請求,如果執行緒池數量大于coreSize時,多余的請求都會放入到等待佇列中,等待著corePool中的執行緒執行完成后再來執行等待佇列中的任務,
試想一下,這種場景我們該如何優化?
我們可以修改執行緒池的執行順序為corePool->maxPool->workQueue, 這樣就能夠充分利用CPU資源,提交的任務會被優先執行,當執行緒池中執行緒數量大于maxSize時才會將任務放入等待佇列中,
你就說巧不巧?面試官的這個問題顯然是經過認真思考來提問的,這是一個很有意思的溫恩提,下面就一起看看如何解決吧,
執行緒池運行流程
我們都知道執行緒池執行流程是先corePool再workQueue,最后才是maxPool的一個執行流程,

執行緒池核心引數
在回顧下ThreadPoolExecutor.execute()原始碼前我們先回顧下執行緒池中的幾個重要引數:

我們來看下這幾個引數的定義:
corePoolSize: 執行緒池中核心執行緒數量
maximumPoolSize: 執行緒池中最大執行緒數量
keepAliveTime: 非核心的空閑執行緒等待新任務的時間
unit: 時間單位,配合allowCoreThreadTimeOut也會清理核心執行緒池中的執行緒,
workQueue: 基于Blocking的任務佇列,最好選用有界佇列,指定佇列長度
threadFactory: 執行緒工廠,最好自定義執行緒工廠,可以自定義每個執行緒的名稱
handler: 拒絕策略,默認是AbortPolicy
ThreadPoolExecutor.execute()原始碼分析
我們可以看下execute()如下:

接著來分析下執行程序:
- 第一步:
workerCountOf(c)時間計算當前執行緒池中執行緒的個數,當執行緒個數小于核心執行緒數 - 第二步:執行緒池執行緒數量大于核心執行緒數,此時提交的任務會放入
workQueue中,使用offer()進行操作 - 第三步:
workQueue.offer()執行失敗,新提交的任務會直接執行,addWorker()會判斷如果當前執行緒池數量大于最大執行緒數,則執行拒絕策略
好了,到了這里我們都已經很清楚了,關鍵在于第二步和第三步如何交換順序執行呢?
解決思路
仔細想一想,如果修改workQueue.offer()的實作不就可以達到目的了?我們先來畫圖來看一下:

現在的問題就在于,如果當前執行緒池中coreSize < workCount < maxSize時,一定會先執行offer()操作,
我們如果修改offer的實作是否可以完成執行順序的更換呢?這里也是畫圖來展示一下:

Dubbo中EagerThreadPool解決方案
湊巧Dubbo中也有類似的實作,在Dubbo的EagerThreadPool自定義了一個BlockingQueue,在offer()方法中,如果當前執行緒池數量小于最大執行緒池時,直接回傳false,這里就達到了調節執行緒池執行順序的目的,

原始碼直達:https://github.com/apache/dubbo/blob/master/dubbo-common/src/main/java/org/apache/dubbo/common/threadpool/support/eager/TaskQueue.java
看到這里一切都真相大白了,解決思路以及方案都很簡單,學會了沒有?
這個問題背后還隱藏了一些場景的優化、原始碼的擴展等等知識,果然是一個值得思考的好問題,
子執行緒拋出的例外,主執行緒能感知到么?
問題思考
這個問題其實也很容易回答,也僅僅是一個面試題而已,實際作業中子執行緒的例外不應該由主執行緒來捕獲,
針對這個問題,希望大家清楚的是: 我們要明確執行緒代碼的邊界,異步化程序中,子執行緒拋出的例外應該由子執行緒自己去處理,而不是需要主執行緒感知來協助處理,
解決方案
解決方案很簡單,在虛擬機中,當一個執行緒如果沒有顯式處理例外而拋出時會將該例外事件報告給該執行緒物件的 java.lang.Thread.UncaughtExceptionHandler 進行處理,如果執行緒沒有設定 UncaughtExceptionHandler,則默認會把例外堆疊資訊輸出到終端而使程式直接崩潰,
所以如果我們想在執行緒意外崩潰時做一些處理就可以通過實作 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("我要開始生產Bug了============");
if (count == 10) {
System.out.println(1 / 0);
}
if (count == 20) {
System.out.println("這里是不會執行到的==========");
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());
}
}
執行結果:

UncaughtExceptionHandler 決議
我們來看下Thread中的內部介面UncaughtExceptionHandler:
public class Thread {
......
/**
* 當一個執行緒因未捕獲的例外而即將終止時虛擬機將使用 Thread.getUncaughtExceptionHandler()
* 獲取已經設定的 UncaughtExceptionHandler 實體,并通過呼叫其 uncaughtException(...) 方
* 法而傳遞相關例外資訊,
* 如果一個執行緒沒有明確設定其 UncaughtExceptionHandler,則將其 ThreadGroup 物件作為其
* handler,如果 ThreadGroup 物件對例外沒有什么特殊的要求,則 ThreadGroup 會將呼叫轉發給
* 默認的未捕獲例外處理器(即 Thread 類中定義的靜態未捕獲例外處理器物件),
*
* @see #setDefaultUncaughtExceptionHandler
* @see #setUncaughtExceptionHandler
* @see ThreadGroup#uncaughtException
*/
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* 未捕獲例外崩潰時回呼此方法
*/
void uncaughtException(Thread t, Throwable e);
}
/**
* 靜態方法,用于設定一個默認的全域例外處理器,
*/
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
defaultUncaughtExceptionHandler = eh;
}
/**
* 針對某個 Thread 物件的方法,用于對特定的執行緒進行未捕獲的例外處理,
*/
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
checkAccess();
uncaughtExceptionHandler = eh;
}
/**
* 當 Thread 崩潰時會呼叫該方法獲取當前執行緒的 handler,獲取不到就會呼叫 group(handler 型別),
* group 是 Thread 類的 ThreadGroup 型別屬性,在 Thread 構造中實體化,
*/
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
/**
* 執行緒全域默認 handler,
*/
public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() {
return defaultUncaughtExceptionHandler;
}
......
}
部分內容參考自:https://mp.weixin.qq.com/s/ghnNQnpou6-NemhFjpl4Jg
執行緒池發生了例外改怎樣處理?
執行緒池中執行緒運行程序中出現了例外該怎樣處理呢?執行緒池提交任務有兩種方式,分別是execute()和submit(),這里會依次說明,
ThreadPoolExecutor.runWorker()實作
不管是使用execute()還是submit()提交任務,最終都會執行到ThreadPoolExecutor.runWorker(),我們來看下原始碼(原始碼基于JDK1.8):

我們看到在執行task.run()時,出現例外會直接向上拋出,這里處理的最好的方式就是在我們業務代碼中使用try...catch()來捕獲例外,
FutureTask.run()實作
如果我們使用submit()來提交任務,在ThreadPoolExecutor.runWorker()方法執行時最侄訓呼叫到FutureTask.run()方法里面去,不清楚的小伙伴也可以看下我之前的文章:
執行緒池續:你必須要知道的執行緒池submit()實作原理之FutureTask!

這里可以看到,如果業務代碼拋出例外后,會被catch捕獲到,然后呼叫setExeception()方法:

可以看到其實類似于直接吞掉了,當我們呼叫get()方法的時候例外資訊會包裝到FutureTask內部的變數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()方法,感知例外細節
總結
這篇文章到這里就結束了,不知道小伙伴們有沒有一些感悟或識訓?
通過這幾個面試問題,我也深刻的感受到學習知識要多思考,看原始碼的程序中要多設定一些場景,這樣才會識訓更多,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/162679.html
標籤:Java
