天天看點

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

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

注意!注意!注意!重要的事說三遍

系統性能優化的幾種常用手段是異步和緩存。是以我們常常使用線程池異步處理一些業務。

線程池的使用還是相對比較簡單的,首先建立一個線程池,然後通過execute或submit執行任務。

但魔鬼往往藏于細節之中,稍有不慎就會出錯。本文将會詳細總結線程池容易出錯的五大坑

一、拒絕政策參數知多少 二、拒絕政策使用不當,系統阻塞不可用 三、多任務get()異常時,結果擷取有誤 四、ThreadLocal與線程池搭配使用,上下文缺失 五、父子任務共用同一線程池,系統“饑餓”死鎖

以下為線程池的核心流程【具體内容參考:線程池原理】

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

通過實作RejectedExecutionHandler,就可以作為線程池的拒絕政策使用。

目前官方提供了四種拒絕政策,分别為:

CallerRunsPolicy:由任務調用方執行

AbortPolicy:抛出異常,同樣也是由任務調用方處理異常

DiscardPolicy:丢棄目前任務

DiscardOldestPolicy:丢棄隊列中最老的任務,并執行目前任務

線程池有execute和submit兩種方法執行任務:

execute執行我們最原始的任務;

而submit則不同,先是将我們最原始的任務封裝成FutureTask任務,然後将FutureTask任務交由execute執行

線程池拒絕政策中Runnable r就是execute執行的任務,是以當使用r時就要注意它是我們最原始的任務還是FutureTask任務

前面我們講到submit方法執行任務時,線程池會先封裝任務到FutureTask中,然後我們通過FutureTask的get()方法擷取任務處理的結果

【具體内容參考:一張動圖,徹底懂了execute和submit】

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

FutureTask在被建立時狀态為NEW,任務執行到某個階段就會修改成相應狀态,直到達到最終态。

FutureTask根據狀态變更來辨別任務執行進度的,是以get()方法也是在狀态達到最終态(任務執行成果/異常/被取消/被打斷)時才能傳回結果,否則挂起目前線程等待到達最終态。

問題原因:

1、當任務通過submit方法執行時,會建立FutureTask(此時狀态為NEW)

2、任務被拒絕且拒絕政策為丢棄任務(DiscardOleddestPolicy或DiscardPolicy)時,任務直接被線程池丢棄(此時狀态仍為NEW)

3、當執行get()方法時,由于任務一直處于NEW狀态,沒有達到最終态,線程會一直處于阻塞狀态

解決方案:

問題原因在于:任務無法變成最終态,導緻阻塞。

是以我們可以重寫rejectedExecution方法,将任務置為最終态

FutureTask的cancel方法可以将任務狀态置為CANCELLED或INTERRUPTED

submit方法中,futureTask會捕獲異常,在get()時抛出。

若批量執行多個方法,且for循環get()結果時,捕獲異常要在循環内,而不是循環外。否則會影響其他任務的結果輸出

捕獲異常在循環外,當一個任務get異常時,後續其他任務就不能再擷取結果

是以在循環内捕獲異常,各個任務互相不受影響

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

為防止記憶體洩漏,在使用完ThreadLocal後都會調用remove()清除資料

問題描述:

1、當任務需要調用方線程的ThreadLocal資訊時,通用方式就是将調用方ThreadLocal資訊指派到執行任務的線程中,在任務執行結束後調用remove()清除資料

2、同時任務恰好被線程池拒絕,且使用的拒絕政策是CallerRunsPolicy時,任務會被調用方線程執行。

3、若此時任務執行結束後仍調用remove()清除資料,清除的就會是調用方的ThreadLocal資料。

調用方ThreadLocal資料被清除,資料丢失在工作中将會是災難性的。

問題出現的原因是任務由于被拒絕,導緻誤删除了調用方ThreadLocal資料

是以可以在任務執行時判斷執行線程是否為調用方線程。

若是則不用set()複制和remove()清空資料

待執行的任務通過重寫process方法,并根據sameThread判斷是否和主線程一緻,一緻則不重複設定相同的threadLocal和删除threadLocal

A方法調用B方法,AB方法稱為父子任務。

當他們都被同一個線程池執行時,一定條件下會出現以下場景:

1、父任務擷取到線程池線程執行,而子任務則被暫存到隊列中

2、當父任務占滿了線程池所有的線程,等待子任務傳回結果後,結束父任務

3、此時子任務由于在隊列中,一直不能等到線程來處理,導緻不能從隊列中釋放

4、父子任務互相等待,進而造成“饑餓”死鎖

我們舉一個簡單例子:

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

細數線程池五大坑,一不小心線上就崩了
1、線程池建立核心線程,并執行A方法 2、執行到B方法時,将B交給線程池執行,由于沒有多餘線程,是以暫存隊列 3、A任務等待B任務執行完,B任務等待A任務釋放線程。進而互相等待,造成“饑餓”死鎖

問題原因在于互相等待,是以隻要保證類似的父子任務不要被同一線程池執行即可

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

如果這個辦法對您有用,或者您希望持續關注,也可以掃描下方二維碼或者在微信公衆号中搜尋【碼路無涯】