線程池用多了總會出現(xiàn)些詭異問題,特別是當任務間的關系比較復雜時,經(jīng)常會出現(xiàn)讓你想象不到問題,比如這次出現(xiàn)的這個問題。
突然間,系統(tǒng)出現(xiàn)大量報警,具體信息如下:
圖片
從拋出的異常可知,提交量較大導致線程池資源被耗盡,從而觸發(fā)了線程池的拒絕策略,直接拋出了 RejectedExecutionException。
開始的時候,小艾認為等高峰流量過去后,系統(tǒng)便能恢復正常。可出乎意料的是,系統(tǒng)一直沒有恢復,那么流量已經(jīng)將至個位數(shù),請求也是 100% 失敗,同時該節(jié)點的大量后臺任務都出現(xiàn)異常。沒有辦法,為了快速止損,不得已對異常節(jié)點進行重啟,系統(tǒng)隨之恢復正常,日志輸入如下:
圖片
其他的后臺任務也恢復正常。
驚魂初定的小艾找到出問題的代碼如下:
@GetMapping("syncSubmit")public RestResult<String> syncSubmit(String taskName){ this.executeService.submit(new ParentTask()); return RestResult.success("提交成功");}class ParentTask implements Callable<Boolean>{ @Override public Boolean call() throws Exception { Future<A> aFuture = executeService.submit(new FetchAChildTask()); doSomeThing(500); Future<B> bFuture = executeService.submit(new FetchBChildTask()); doSomeThing(500); C c = buildC(aFuture.get(), bFuture.get()); Future<Boolean> cFuture = executeService.submit(new SaveCChildTask(c)); return cFuture.get(); }}
代碼的邏輯非常簡單,核心流程如下圖所示:
圖片
核心流程為:
邏輯非常簡單,唯一的復雜點在于:==多處任務提交使用了統(tǒng)一線程池。==
【背景】考慮到線程池是系統(tǒng)中最寶貴的資源,公司“大牛”封裝了一個全局的 GlobalExecuteService 服務,并制定規(guī)范要求所有異步任務統(tǒng)一使用 GlobalExecuteService 來完成。如果需要構建自己的線程池,需要向他提交審批,只有在審批后才能創(chuàng)建新的線程池。
線程池處于什么狀態(tài)?為什么所有異步任務都無法提交?
這是一個比較燒腦的問題,單盤邏輯沒有什么頭緒,沒有辦法只能將現(xiàn)場 dump 下來進行分析。
第一個問題:線程池線程都處于什么狀態(tài),線程棧信息如下:
圖片
從日志中可知:
接下來,需要進一步確認 FutureTask#10 具體處于什么狀態(tài),從內存堆中找到 FutureTask#10 對象,詳細信息如下:
圖片
從日志中可以看出:
排查到這里,真相浮出水面:GlobalExecuteService 中的線程,正在等待 GlobalExecuteService 阻塞隊列的任務完成。
具體如下圖所示:
圖片
線程池中的所有工作線程都在等待阻塞隊列的任務完成,由于沒有可用的工作線程,阻塞隊列中的任務永遠都不會被執(zhí)行。
這就是典型的死鎖!!!
費了老大勁終于定位問題,解決思路也就變的明了:不要向自己運行的線程池提交任務。
圖解如下:
圖片
線程池不會向自己提交任務,而是將任務提交到其他線程池。
問題修復變的簡單,我們需要:
具體代碼如下:
@Autowiredprivate GlobalExecuteService executeService;// 創(chuàng)建新的線程池服務@Autowiredprivate SubExecuteService subExecuteService;@GetMapping("syncSubmit")public RestResult<String> syncSubmit(String taskName){ this.executeService.submit(new ParentTask()); return RestResult.success("提交成功");}class ParentTask implements Callable<Boolean>{ @Override public Boolean call() throws Exception { log.info("Begin to Run Parent Task"); // 向新的線程池服務提交任務 Future<A> aFuture = subExecuteService.submit(new FetchAChildTask()); doSomeThing(500); // 向新的線程池服務提交任務 Future<B> bFuture = subExecuteService.submit(new FetchBChildTask()); doSomeThing(500); C c = buildC(aFuture.get(), bFuture.get()); // 向新的線程池服務提交任務 Future<Boolean> cFuture = subExecuteService.submit(new SaveCChildTask(c)); Boolean result = cFuture.get(); log.info("End to Run Parent Task"); return result; }}
手工拆分線程池確實能解決這個場景的問題,但由于 GlobalExecuteService 服務已經(jīng)使用很長時間,任務間的關系錯綜復雜,很難一次性排查并修復所有問題,同時隨著邏輯的變化未來仍舊會出現(xiàn)類似的問題。
那最佳方案是什么?
讓 GlobalExecuteService 具備多級管理能力。核心代碼如下:
@Servicepublic class GlobalExecuteServiceV2 { // 記錄當前線程運行級別,默認 0,表示當前線程非該類管理的線程池線程 private static final ThreadLocal<Integer> LEVEL_HOLDER = ThreadLocal.withInitial(()->0); // 一級線程池 private ExecutorService executorServiceLeve1; // 二級線程池 private ExecutorService executorServiceLeve2; // 默認線程池 private ExecutorService defExecutorService; @PostConstruct public void init() { // 省略線程池初始化邏輯 } public <T> Future<T> submit(Callable<T> callable){ // 獲取當前線程的運行級別 Integer level = LEVEL_HOLDER.get(); // 根據(jù)當前運行級別,計算子任務所使用的線程池 ExecutorService executorService = getNextExecutorServiceByLevel(level); // 為子任務分配運行級別 CallableWrapper<T> callableWrapper = new CallableWrapper<>(level + 1, callable); // 提交任務 return executorService.submit(callableWrapper); } private ExecutorService getNextExecutorServiceByLevel(Integer level) { if (level == 0){ return executorServiceLeve1; } if (level == 1){ return executorServiceLeve2; } return defExecutorService; } class CallableWrapper<T> implements Callable<T>{ private final Integer level; private final Callable<T> callable; CallableWrapper(Integer level, Callable<T> callable) { this.level = level; this.callable = callable; } @Override public T call() throws Exception { try { // 為線程池綁定運行級別 LEVEL_HOLDER.set(level); return callable.call(); }finally { // 清理線程池運行級別 LEVEL_HOLDER.remove(); } } }}
核心設計如下:
為了演示方便,僅定義了 3 級線程池,通常情況下足夠業(yè)務使用,但需要注意:
- 超過三級提交,仍舊有可以出現(xiàn)死鎖的情況,可以通過日志方式及時暴露問題
- 如不放心,可以升級為 “無限極” 設計,及使用 List<ExecutorService> 對線程池進行統(tǒng)一管理,并根據(jù) Level 完成線程池的動態(tài)創(chuàng)建
代碼倉庫:
https://gitee.com/litao851025/learnFromBug
代碼地址:
https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/thread/deadlock
本文鏈接:http://www.www897cc.com/showinfo-26-70483-0.html故障現(xiàn)場 | 這個死鎖出奇的詭異
聲明:本網(wǎng)頁內容旨在傳播知識,若有侵權等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
下一篇: 我們一起聊聊React列表渲染與Key