日韩成人免费在线_国产成人一二_精品国产免费人成电影在线观..._日本一区二区三区久久久久久久久不

當前位置:首頁 > 科技  > 軟件

故障現(xiàn)場 | 這個死鎖出奇的詭異

來源: 責編: 時間:2024-02-01 12:52:21 222觀看
導讀1. 問題&分析線程池用多了總會出現(xiàn)些詭異問題,特別是當任務間的關系比較復雜時,經(jīng)常會出現(xiàn)讓你想象不到問題,比如這次出現(xiàn)的這個問題。1.1. 案例突然間,系統(tǒng)出現(xiàn)大量報警,具體信息如下:圖片從拋出的異常可知,提交量較大導致

1. 問題&分析

線程池用多了總會出現(xiàn)些詭異問題,特別是當任務間的關系比較復雜時,經(jīng)常會出現(xiàn)讓你想象不到問題,比如這次出現(xiàn)的這個問題。rul28資訊網(wǎng)——每日最新資訊28at.com

1.1. 案例

突然間,系統(tǒng)出現(xiàn)大量報警,具體信息如下:rul28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片rul28資訊網(wǎng)——每日最新資訊28at.com

從拋出的異常可知,提交量較大導致線程池資源被耗盡,從而觸發(fā)了線程池的拒絕策略,直接拋出了 RejectedExecutionException。rul28資訊網(wǎng)——每日最新資訊28at.com

開始的時候,小艾認為等高峰流量過去后,系統(tǒng)便能恢復正常。可出乎意料的是,系統(tǒng)一直沒有恢復,那么流量已經(jīng)將至個位數(shù),請求也是 100% 失敗,同時該節(jié)點的大量后臺任務都出現(xiàn)異常。沒有辦法,為了快速止損,不得已對異常節(jié)點進行重啟,系統(tǒng)隨之恢復正常,日志輸入如下:rul28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片rul28資訊網(wǎng)——每日最新資訊28at.com

其他的后臺任務也恢復正常。rul28資訊網(wǎng)——每日最新資訊28at.com

驚魂初定的小艾找到出問題的代碼如下:rul28資訊網(wǎng)——每日最新資訊28at.com

@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();    }}

代碼的邏輯非常簡單,核心流程如下圖所示:rul28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片rul28資訊網(wǎng)——每日最新資訊28at.com

核心流程為:rul28資訊網(wǎng)——每日最新資訊28at.com

  1. 提交異步任務,并行獲取 A 和 B
  2. 線程同步處理一下耗時操作
  3. 獲取 A 和 B 結果后,構建新對象 C
  4. 將 C 保存到數(shù)據(jù)庫

邏輯非常簡單,唯一的復雜點在于:==多處任務提交使用了統(tǒng)一線程池。==rul28資訊網(wǎng)——每日最新資訊28at.com

【背景】考慮到線程池是系統(tǒng)中最寶貴的資源,公司“大牛”封裝了一個全局的 GlobalExecuteService 服務,并制定規(guī)范要求所有異步任務統(tǒng)一使用 GlobalExecuteService 來完成。如果需要構建自己的線程池,需要向他提交審批,只有在審批后才能創(chuàng)建新的線程池。rul28資訊網(wǎng)——每日最新資訊28at.com

1.2. 問題分析

線程池處于什么狀態(tài)?為什么所有異步任務都無法提交?rul28資訊網(wǎng)——每日最新資訊28at.com

這是一個比較燒腦的問題,單盤邏輯沒有什么頭緒,沒有辦法只能將現(xiàn)場 dump 下來進行分析。rul28資訊網(wǎng)——每日最新資訊28at.com

第一個問題:線程池線程都處于什么狀態(tài),線程棧信息如下:rul28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片rul28資訊網(wǎng)——每日最新資訊28at.com

從日志中可知:rul28資訊網(wǎng)——每日最新資訊28at.com

  1. 線程池中的線程全部處于 WAITING,也就是等待狀態(tài);
  2. 展開 Thread-1 棧信息,發(fā)現(xiàn)線程再調用 future.get 操作時出現(xiàn)阻塞
  3. 實際等待對象為 FutrueTask#10

接下來,需要進一步確認 FutureTask#10 具體處于什么狀態(tài),從內存堆中找到 FutureTask#10 對象,詳細信息如下:rul28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片rul28資訊網(wǎng)——每日最新資訊28at.com

從日志中可以看出:rul28資訊網(wǎng)——每日最新資訊28at.com

  1. FutureTask#10 是 LinkedBlockingQueue 的 Node 節(jié)點持有,也就是 FutureTask#10 處于等待隊列中
  2. 該阻塞隊列屬于 GlobalExecuteService 所有

排查到這里,真相浮出水面:GlobalExecuteService 中的線程,正在等待 GlobalExecuteService 阻塞隊列的任務完成。rul28資訊網(wǎng)——每日最新資訊28at.com

具體如下圖所示:rul28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片rul28資訊網(wǎng)——每日最新資訊28at.com

線程池中的所有工作線程都在等待阻塞隊列的任務完成,由于沒有可用的工作線程,阻塞隊列中的任務永遠都不會被執(zhí)行。rul28資訊網(wǎng)——每日最新資訊28at.com

這就是典型的死鎖!!!rul28資訊網(wǎng)——每日最新資訊28at.com

2. 解決方案

費了老大勁終于定位問題,解決思路也就變的明了:不要向自己運行的線程池提交任務。rul28資訊網(wǎng)——每日最新資訊28at.com

圖解如下:rul28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片rul28資訊網(wǎng)——每日最新資訊28at.com

線程池不會向自己提交任務,而是將任務提交到其他線程池。rul28資訊網(wǎng)——每日最新資訊28at.com

2.1. 手工拆分線程池

問題修復變的簡單,我們需要:rul28資訊網(wǎng)——每日最新資訊28at.com

  1. 先建一個子線程池服務
  2. 父線程池向子線程池提交任務

具體代碼如下:rul28資訊網(wǎng)——每日最新資訊28at.com

@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;    }}

2.2. 多級任務管理

手工拆分線程池確實能解決這個場景的問題,但由于 GlobalExecuteService 服務已經(jīng)使用很長時間,任務間的關系錯綜復雜,很難一次性排查并修復所有問題,同時隨著邏輯的變化未來仍舊會出現(xiàn)類似的問題。rul28資訊網(wǎng)——每日最新資訊28at.com

那最佳方案是什么?rul28資訊網(wǎng)——每日最新資訊28at.com

讓 GlobalExecuteService 具備多級管理能力。核心代碼如下:rul28資訊網(wǎng)——每日最新資訊28at.com

@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();            }        }    }}

核心設計如下:rul28資訊網(wǎng)——每日最新資訊28at.com

  1. 使用 ThreadLocal<Integer> LEVEL_HOLDER 記錄當前線程運行的級別,默認為 0 表示任務未在線程池中運行
  2. 提交任務時,通過當前運行級別計算下一級別的線程池
  • 當前級別為0,返回 Level1 線程池
  • 當前級別為1,返回 Level2 線程池
  • 其他基本,返回 默認 線程池
  1. 通過 CallableWrapper 自動將任務運行的 Level 綁定到當前線程上下文
  2. 任務執(zhí)行前,使用 LEVEL_HOLDER.set(level) 完成運行 level 的設置
  3. 任務執(zhí)行后,使用 LEVEL_HOLDER.remove() 完成運行 level 的清理

為了演示方便,僅定義了 3 級線程池,通常情況下足夠業(yè)務使用,但需要注意:rul28資訊網(wǎng)——每日最新資訊28at.com

  • 超過三級提交,仍舊有可以出現(xiàn)死鎖的情況,可以通過日志方式及時暴露問題
  • 如不放心,可以升級為 “無限極” 設計,及使用 List<ExecutorService> 對線程池進行統(tǒng)一管理,并根據(jù) Level 完成線程池的動態(tài)創(chuàng)建

3. 示例&源碼

代碼倉庫:rul28資訊網(wǎng)——每日最新資訊28at.com

https://gitee.com/litao851025/learnFromBugrul28資訊網(wǎng)——每日最新資訊28at.com

代碼地址:rul28資訊網(wǎng)——每日最新資訊28at.com

https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/thread/deadlockrul28資訊網(wǎng)——每日最新資訊28at.com

本文鏈接:http://www.www897cc.com/showinfo-26-70483-0.html故障現(xiàn)場 | 這個死鎖出奇的詭異

聲明:本網(wǎng)頁內容旨在傳播知識,若有侵權等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com

上一篇: CSS 和 SVG 實現(xiàn)彩色圖片陰影

下一篇: 我們一起聊聊React列表渲染與Key

標簽:
  • 熱門焦點
Top 主站蜘蛛池模板: 西林县| 吉林省| 新乐市| 呼伦贝尔市| 云南省| 吐鲁番市| 平阳县| 额尔古纳市| 荣昌县| 涡阳县| 平武县| 滨州市| 黎平县| 神池县| 泰州市| 通渭县| 波密县| 志丹县| 扬州市| 崇文区| 南和县| 洮南市| 承德市| 靖江市| 县级市| 靖边县| 古田县| 灵川县| 横峰县| 岗巴县| 宁晋县| 漠河县| 绥宁县| 阿图什市| 黄骅市| 海原县| 平舆县| 贡嘎县| 辽中县| 湘阴县| 荣昌县|