細數(shù)線程池五大坑,一不小心線上就崩了

系統(tǒng)性能優(yōu)化的幾種常用手段是異步和緩存濒旦。因此我們常常使用線程池異步處理一些業(yè)務株旷。
線程池的使用還是相對比較簡單的,首先創(chuàng)建一個線程池尔邓,然后通過execute或submit執(zhí)行任務晾剖。
但魔鬼往往藏于細節(jié)之中,稍有不慎就會出錯梯嗽。本文將會詳細總結(jié)線程池容易出錯的五大坑

一齿尽、拒絕策略參數(shù)知多少
二、拒絕策略使用不當灯节,系統(tǒng)阻塞不可用
三雕什、多任務get()異常時,結(jié)果獲取有誤
四、ThreadLocal與線程池搭配使用显晶,上下文缺失
五贷岸、父子任務共用同一線程池,系統(tǒng)“饑餓”死鎖

以下為線程池的核心流程【具體內(nèi)容參考:線程池原理

image


一磷雇、拒絕策略參數(shù)知多少

我們都知道偿警,當任務過多,線程池處理不過來時會被拒絕唯笙,進入拒絕策略

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

通過實現(xiàn)RejectedExecutionHandler螟蒸,就可以作為線程池的拒絕策略使用盒使。
目前官方提供了四種拒絕策略,分別為:

  • CallerRunsPolicy:由任務調(diào)用方執(zhí)行
  • AbortPolicy:拋出異常七嫌,同樣也是由任務調(diào)用方處理異常
  • DiscardPolicy:丟棄當前任務
  • DiscardOldestPolicy:丟棄隊列中最老的任務少办,并執(zhí)行當前任務

線程池有execute和submit兩種方法執(zhí)行任務:
execute執(zhí)行我們最原始的任務;
而submit則不同诵原,先是將我們最原始的任務封裝成FutureTask任務英妓,然后將FutureTask任務交由execute執(zhí)行

線程池拒絕策略中Runnable r就是execute執(zhí)行的任務,因此當使用r時就要注意它是我們最原始的任務還是FutureTask任務


二绍赛、拒絕策略使用不當蔓纠,系統(tǒng)阻塞不可用

前面我們講到submit方法執(zhí)行任務時,線程池會先封裝任務到FutureTask中吗蚌,然后我們通過FutureTask的get()方法獲取任務處理的結(jié)果
【具體內(nèi)容參考:一張動圖腿倚,徹底懂了execute和submit

Possible state transitions:
NEW -> COMPLETING -> NORMAL(任務執(zhí)行完成)
NEW ->COMPLETING -> EXCEPTIONAL(任務拋出異常)
NEW -> CANCELLED(任務被取消)
NEW -> INTERRUPTING -> INTERRUPTED(任務被打斷)

FutureTask在被創(chuàng)建時狀態(tài)為NEW,任務執(zhí)行到某個階段就會修改成相應狀態(tài)蚯妇,直到達到最終態(tài)敷燎。

FutureTask根據(jù)狀態(tài)變更來標識任務執(zhí)行進度的,因此get()方法也是在狀態(tài)達到最終態(tài)(任務執(zhí)行成果/異常/被取消/被打斷)時才能返回結(jié)果箩言,否則掛起當前線程等待到達最終態(tài)硬贯。

問題原因:
1、當任務通過submit方法執(zhí)行時分扎,會創(chuàng)建FutureTask(此時狀態(tài)為NEW)
2澄成、任務被拒絕且拒絕策略為丟棄任務(DiscardOleddestPolicy或DiscardPolicy)時胧洒,任務直接被線程池丟棄(此時狀態(tài)仍為NEW)
3畏吓、當執(zhí)行g(shù)et()方法時,由于任務一直處于NEW狀態(tài)卫漫,沒有達到最終態(tài)菲饼,線程會一直處于阻塞狀態(tài)

解決方案:
問題原因在于:任務無法變成最終態(tài),導致阻塞列赎。
因此我們可以重寫rejectedExecution方法,將任務置為最終態(tài)
FutureTask的cancel方法可以將任務狀態(tài)置為CANCELLED或INTERRUPTED

public static RejectedExecutionHandler customDiscardPolicy () {
  return new DiscardPolicy() {
     @Override
     public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
          if (!e.isShutdown()) {
              if (r != null && r instanceof FutureTask) {
                  ((FutureTask) r).cancel(true);
               }
           }
      }
  };
}


三、多任務get()異常時,結(jié)果獲取有誤

submit方法中黍聂,futureTask會捕獲異常腹纳,在get()時拋出。
若批量執(zhí)行多個方法诗越,且for循環(huán)get()結(jié)果時砖瞧,捕獲異常要在循環(huán)內(nèi),而不是循環(huán)外嚷狞。否則會影響其他任務的結(jié)果輸出

捕獲異常在循環(huán)外块促,當一個任務get異常時荣堰,后續(xù)其他任務就不能再獲取結(jié)果

List<TaskResult> taskResultList = new ArrayList<>();
try {
    for (Future<TaskResult> future : futureList) {
        if (future == null) {continue;}
        TaskResult result = future.get();
        taskResultList.add(result);
    }
} catch (Throwable t) {
    //這種場景下,當一個任務get異常時竭翠,后續(xù)其他任務就不能再獲取結(jié)果
    LOGGER.error("任務執(zhí)行異常", t);
}

因此在循環(huán)內(nèi)捕獲異常振坚,各個任務互相不受影響

List<TaskResult> taskResultList = new ArrayList<>();
for (Future<TaskResult> future : futureList) {
    try {
        if (future == null) {continue;}
        TaskResult result = future.get();
        taskResultList.add(result);
    } catch (Throwable t) {
        LOGGER.error("任務執(zhí)行異常", t);
    }
}


四、ThreadLocal與線程池搭配使用斋扰,上下文缺失

ThreadLocal的使用一般都是這幾個方法:

private final static ThreadLocal<CacheInfo> cacheInfoThreadLocal = new ThreadLocal<CacheInfo>();

cacheInfoThreadLocal.set(cacheInfo);
cacheInfoThreadLocal.get();
cacheInfoThreadLocal.remove();

為防止內(nèi)存泄漏渡八,在使用完ThreadLocal后都會調(diào)用remove()清除數(shù)據(jù)

問題描述:
1、當任務需要調(diào)用方線程的ThreadLocal信息時褥实,通用方式就是將調(diào)用方ThreadLocal信息賦值到執(zhí)行任務的線程中呀狼,在任務執(zhí)行結(jié)束后調(diào)用remove()清除數(shù)據(jù)
2、同時任務恰好被線程池拒絕损离,且使用的拒絕策略是CallerRunsPolicy時哥艇,任務會被調(diào)用方線程執(zhí)行。
3僻澎、若此時任務執(zhí)行結(jié)束后仍調(diào)用remove()清除數(shù)據(jù)貌踏,清除的就會是調(diào)用方的ThreadLocal數(shù)據(jù)。
調(diào)用方ThreadLocal數(shù)據(jù)被清除窟勃,數(shù)據(jù)丟失在工作中將會是災難性的祖乳。

解決方案:
問題出現(xiàn)的原因是任務由于被拒絕,導致誤刪除了調(diào)用方ThreadLocal數(shù)據(jù)
因此可以在任務執(zhí)行時判斷執(zhí)行線程是否為調(diào)用方線程秉氧。
若是則不用set()復制和remove()清空數(shù)據(jù)

public abstract class ParallelCallableTask<V> implements Callable<V> {
    //調(diào)用方線程名稱
    private String mainThreadName;
    
    public ParallelCallableTask() {
        mainThreadName = Thread.currentThread().getName();
    }

    @Override
    public V call() throws Exception {
        //是否為同一線程
        boolean sameThread = sameThread();
        return proccess(sameThread);
    }

    /**判斷 調(diào)用方線程 和 執(zhí)行線程 是否為同一線程*/
    private boolean sameThread () {
        String curThreadName = Thread.currentThread().getName();
        return curThreadName.equals(mainThreadName);
    }
    
    //任務重寫這個方法并根據(jù)sameThread判斷是否需要set和remove調(diào)用方線程的ThreadLocal數(shù)據(jù)
    public abstract V proccess(boolean sameThread);
}

待執(zhí)行的任務通過重寫process方法眷昆,并根據(jù)sameThread判斷是否和主線程一致,一致則不重復設置相同的threadLocal和刪除threadLocal


五汁咏、父子任務共用同一線程池亚斋,系統(tǒng)“饑餓”死鎖

A方法調(diào)用B方法,AB方法稱為父子任務攘滩。

當他們都被同一個線程池執(zhí)行時帅刊,一定條件下會出現(xiàn)以下場景:
1、父任務獲取到線程池線程執(zhí)行漂问,而子任務則被暫存到隊列中
2赖瞒、當父任務占滿了線程池所有的線程,等待子任務返回結(jié)果后蚤假,結(jié)束父任務
3栏饮、此時子任務由于在隊列中,一直不能等到線程來處理磷仰,導致不能從隊列中釋放
4袍嬉、父子任務互相等待,從而造成“饑餓”死鎖

我們舉一個簡單例子:

假設線程池參數(shù)設置為:核心和最大線程數(shù)為1芒划,隊列容量為1

A方法內(nèi)調(diào)用B方法:
A() {
   B()冬竟;
}

現(xiàn)在父子任務都被同一個線程池進行調(diào)用欧穴,整個流程為(如圖所示):


1、線程池創(chuàng)建核心線程泵殴,并執(zhí)行A方法
2涮帘、執(zhí)行到B方法時,將B交給線程池執(zhí)行笑诅,由于沒有多余線程调缨,因此暫存隊列
3、A任務等待B任務執(zhí)行完吆你,B任務等待A任務釋放線程弦叶。從而互相等待,造成“饑餓”死鎖

解決方案:

問題原因在于互相等待妇多,因此只要保證類似的父子任務不要被同一線程池執(zhí)行即可

------The End------


如果這個辦法對您有用伤哺,或者您希望持續(xù)關(guān)注,可以在wx公眾號中搜索【碼路無涯】者祖,期待你的到來

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末立莉,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子七问,更是在濱河造成了極大的恐慌蜓耻,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件械巡,死亡現(xiàn)場離奇詭異刹淌,居然都是意外死亡,警方通過查閱死者的電腦和手機讥耗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進店門有勾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人葛账,你說我怎么就攤上這事柠衅∑と剩” “怎么了籍琳?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長贷祈。 經(jīng)常有香客問我趋急,道長,這世上最難降的妖魔是什么势誊? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任呜达,我火速辦了婚禮,結(jié)果婚禮上粟耻,老公的妹妹穿的比我還像新娘查近。我一直安慰自己眉踱,他們只是感情好,可當我...
    茶點故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布霜威。 她就那樣靜靜地躺著谈喳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪戈泼。 梳的紋絲不亂的頭發(fā)上婿禽,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天,我揣著相機與錄音大猛,去河邊找鬼扭倾。 笑死,一個胖子當著我的面吹牛挽绩,可吹牛的內(nèi)容都是我干的膛壹。 我是一名探鬼主播,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼唉堪,長吁一口氣:“原來是場噩夢啊……” “哼恢筝!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起巨坊,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤撬槽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后趾撵,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體侄柔,經(jīng)...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年占调,在試婚紗的時候發(fā)現(xiàn)自己被綠了暂题。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,769評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡究珊,死狀恐怖薪者,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情剿涮,我是刑警寧澤言津,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站取试,受9級特大地震影響悬槽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瞬浓,卻給世界環(huán)境...
    茶點故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一初婆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦磅叛、人聲如沸屑咳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乔宿。三九已至,卻和暖如春访雪,著一層夾襖步出監(jiān)牢的瞬間详瑞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工臣缀, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留坝橡,地道東北人。 一個月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓精置,卻偏偏與公主長得像计寇,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子脂倦,可洞房花燭夜當晚...
    茶點故事閱讀 44,678評論 2 354

推薦閱讀更多精彩內(nèi)容