面試官:線程池如何按照core座哩、max徒扶、queue的執(zhí)行循序去執(zhí)行?(內(nèi)附詳細解析)

前言

這是一個真實的面試題根穷。

前幾天一個朋友在群里分享了他剛剛面試候選者時問的問題:"線程池如何按照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)實工作場景中是否有這種需求浸须?之前也看到過一份簡歷也寫到過這個問題:

場景描述.png

一個線程池執(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í)行流程是先corePoolworkQueue,最后才是maxPool的一個執(zhí)行流程库快。

執(zhí)行流程.png

線程池核心參數(shù)

在回顧下ThreadPoolExecutor.execute()源碼前我們先回顧下線程池中的幾個重要參數(shù):

線程池核心參數(shù).png

我們來看下這幾個參數(shù)的定義:
corePoolSize: 線程池中核心線程數(shù)量
maximumPoolSize: 線程池中最大線程數(shù)量
keepAliveTime: 非核心的空閑線程等待新任務的時間
unit: 時間單位摸袁。配合allowCoreThreadTimeOut也會清理核心線程池中的線程。
workQueue: 基于Blocking的任務隊列义屏,最好選用有界隊列靠汁,指定隊列長度
threadFactory: 線程工廠蜂大,最好自定義線程工廠,可以自定義每個線程的名稱
handler: 拒絕策略蝶怔,默認是AbortPolicy

ThreadPoolExecutor.execute()源碼分析

我們可以看下execute()如下:

execute執(zhí)行源碼.png

接著來分析下執(zhí)行過程:

  1. 第一步:workerCountOf(c)時間計算當前線程池中線程的個數(shù)奶浦,當線程個數(shù)小于核心線程數(shù)
  2. 第二步:線程池線程數(shù)量大于核心線程數(shù),此時提交的任務會放入workQueue中添谊,使用offer()進行操作
  3. 第三步:workQueue.offer()執(zhí)行失敗财喳,新提交的任務會直接執(zhí)行,addWorker()會判斷如果當前線程池數(shù)量大于最大線程數(shù)斩狱,則執(zhí)行拒絕策略

好了耳高,到了這里我們都已經(jīng)很清楚了,關鍵在于第二步和第三步如何交換順序執(zhí)行呢所踊?

解決思路

仔細想一想泌枪,如果修改workQueue.offer()的實現(xiàn)不就可以達到目的了?我們先來畫圖來看一下:

問題思路.png

現(xiàn)在的問題就在于秕岛,如果當前線程池中coreSize < workCount < maxSize時碌燕,一定會先執(zhí)行offer()操作。

我們?nèi)绻薷?code>offer的實現(xiàn)是否可以完成執(zhí)行順序的更換呢继薛?這里也是畫圖來展示一下:

解決方式.png

Dubbo中EagerThreadPool解決方案

湊巧Dubbo中也有類似的實現(xiàn)修壕,在DubboEagerThreadPool自定義了一個BlockingQueue,在offer()方法中遏考,如果當前線程池數(shù)量小于最大線程池時慈鸠,直接返回false,這里就達到了調(diào)節(jié)線程池執(zhí)行順序的目的灌具。

dubbo中解決方案.png

源碼直達https://github.com/apache/dubbo/blob/master/dubbo-common/src/main/java/org/apache/dubbo/common/threadpool/support/eager/TaskQueue.java

看到這里一切都真相大白了青团,解決思路以及方案都很簡單,學會了沒有咖楣?

這個問題背后還隱藏了一些場景的優(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í)行結果:


執(zhí)行結果.png

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):

runWorker().png

我們看到在執(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拘领!

FutureTask.run().png

這里可以看到,如果業(yè)務代碼拋出異常后樱调,會被catch捕獲到约素,然后調(diào)用setExeception()方法:

FutureTask.setException().png

可以看到其實類似于直接吞掉了届良,當我們調(diào)用get()方法的時候異常信息會包裝到FutureTask內(nèi)部的變量outcome中,我們也會獲取到對應的異常信息圣猎。

ThreadPoolExecutor.runWorker()最后finally中有一個afterExecute()鉤子方法士葫,如果我們重寫了afterExecute()方法,就可以獲取到子線程拋出的具體異常信息Throwable了送悔。

結論

對于線程池慢显、包括線程的異常處理推薦以下方式:

  1. 直接使用try/catch,這個也是最推薦的方式
  2. 在我們構造線程池的時候欠啤,重寫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é)

總結

這篇文章到這里就結束了跪妥,不知道小伙伴們有沒有一些感悟或收獲鞋喇?

通過這幾個面試問題,我也深刻的感受到學習知識要多思考眉撵,看源碼的過程中要多設置一些場景侦香,這樣才會收獲更多。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末纽疟,一起剝皮案震驚了整個濱河市罐韩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌污朽,老刑警劉巖散吵,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蟆肆,居然都是意外死亡矾睦,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門炎功,熙熙樓的掌柜王于貴愁眉苦臉地迎上來枚冗,“玉大人,你說我怎么就攤上這事蛇损×尬拢” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵淤齐,是天一觀的道長股囊。 經(jīng)常有香客問我,道長更啄,這世上最難降的妖魔是什么稚疹? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮祭务,結果婚禮上贫堰,老公的妹妹穿的比我還像新娘穆壕。我一直安慰自己,他們只是感情好其屏,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布喇勋。 她就那樣靜靜地躺著,像睡著了一般偎行。 火紅的嫁衣襯著肌膚如雪川背。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天蛤袒,我揣著相機與錄音熄云,去河邊找鬼。 笑死妙真,一個胖子當著我的面吹牛缴允,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播珍德,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼练般,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了锈候?” 一聲冷哼從身側響起薄料,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎泵琳,沒想到半個月后摄职,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡获列,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年谷市,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片击孩。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡歌懒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出溯壶,到底是詐尸還是另有隱情,我是刑警寧澤甫男,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布且改,位于F島的核電站,受9級特大地震影響板驳,放射性物質(zhì)發(fā)生泄漏又跛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一若治、第九天 我趴在偏房一處隱蔽的房頂上張望慨蓝。 院中可真熱鬧感混,春花似錦、人聲如沸礼烈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽此熬。三九已至庭呜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間犀忱,已是汗流浹背募谎。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留阴汇,地道東北人数冬。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像搀庶,于是被迫代替她去往敵國和親拐纱。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355