執行緒池 操作不規範導致的死鎖問題
阿新 • • 發佈:2022-03-31
起因
利潤校驗地方,我封裝了底層的利潤校驗,查詢京東價格用了自定義執行緒池A批量去查詢,然後別的同事也需要用到我的利潤校驗,他也使用了執行緒池A去處理邏輯(去進行利潤校驗,但是我的利潤校驗也是用的執行緒池A),這就導致,上層的執行緒池A去新增任務,上層的執行緒池由於任務比較多,或者多次執行,導致執行緒池的核心執行緒一直在執行,然後下層任務放到了佇列中,一直等待核心執行緒執行完畢,但是核心執行緒又在等待下層利潤校驗的返回值。然而上層的執行緒池A做的任務是呼叫下層的執行緒池A,即上層使用了執行緒池A呼叫下層的利潤校驗,利潤校驗也是用的執行緒池A。結果就導致,上層的邏輯需要等待下層邏輯的返回,但是下層的邏輯一直得到不到執行(需要等待上層釋放資源,讓出空間,下層才有機會執行),就造成了彼此相互等待。
查因
流量峰值時發現大量呼叫超時,通過鏈路追蹤鎖定超時發生的節點,隔離節點後,在Pod中使用jstack命令追蹤程序
jstack -l 1 |grep "java.lang.Thread.State"|sort -nr|uniq -c
發現有大量執行緒阻塞在WAITING狀態
11 java.lang.Thread.State: WAITING (parking)
2 java.lang.Thread.State: WAITING (on object monitor)
8 java.lang.Thread.State: RUNNABLE
Dump執行緒資訊:jstack -l 1 > stack.log,分析WAITING狀態的執行緒 ,發現 執行緒池出現了巢狀提交 造成死鎖
分析 舉例
核心程式碼如下,外層任務
public void test() throws ExecutionException, InterruptedException { List<Future<String>> outerFutureList = new ArrayList<>(10); for (int i = 0; i < 11; i++) { outerFutureList.add( executorService.submit( new BadTask(executorService) ) ); } // 阻塞等待完成 for (Future<String> outerFuture : outerFutureList) { System.out.println(outerFuture.get()); } executorService.shutdownNow(); }
BadTask類中造成問題的 主要程式碼
private static class BadTask implements Callable<String>{
private BadTask(ExecutorService executorService) {
this.executorService = executorService;
}
@Override
public String call() throws InterruptedException {
// rpc等耗時操作
TimeUnit.MILLISECONDS.sleep(10L);
List<Future<String>> innerFutureList = new ArrayList<>(5);
// 因為有些SDK的介面有引數數量限制,所以多次呼叫,為提高RT,複用執行緒池併發呼叫
for (int i = 0; i < 5; i++) {
innerFutureList.add(
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓這裡又提交了一些Callable到同一個執行緒池
this.executorService.submit(
()->{
// rpc等耗時操作
TimeUnit.MILLISECONDS.sleep(20L);
return "OK";
}
)
);
}
// 阻塞等待全部完成
for (Future<String> innerFuture : innerFutureList) {
innerFuture.get();
}
return "OK";
}
}
結論
完全死鎖的必要條件
- 執行緒池必須有阻塞佇列,亦即有可能部分Runnable物件存在阻塞佇列,實際上發生死鎖的就是Worker裡執行的Runnable和阻塞佇列的Runnable
- 父任務和子任務向同一個執行緒池提交任務【核心原因】
- 父任務阻塞等待所有子任務完成,子任務部分在阻塞佇列裡
- Worker裡全為父任務,且對應的所有子任務都在阻塞佇列裡
破壞以上任何條件,都不會造成完全死鎖,但是隻要符合條件1,2,在某些情況下仍舊會造成執行緒執行慢
解決方法
- 不使用阻塞佇列,即使用同步佇列
java.util.concurrent.SynchronousQueue
- 父任務和子任務使用不同的執行緒池
- 控制併發低於共享執行緒池的核心執行緒數(僅在無法使用上述方案時使用)