1. 程式人生 > 其它 >執行緒池 操作不規範導致的死鎖問題

執行緒池 操作不規範導致的死鎖問題

起因

利潤校驗地方,我封裝了底層的利潤校驗,查詢京東價格用了自定義執行緒池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";
    }
}

結論

完全死鎖的必要條件

  1. 執行緒池必須有阻塞佇列,亦即有可能部分Runnable物件存在阻塞佇列,實際上發生死鎖的就是Worker裡執行的Runnable和阻塞佇列的Runnable
  2. 父任務和子任務向同一個執行緒池提交任務【核心原因】
  3. 父任務阻塞等待所有子任務完成,子任務部分在阻塞佇列裡
  4. Worker裡全為父任務,且對應的所有子任務都在阻塞佇列裡

破壞以上任何條件,都不會造成完全死鎖,但是隻要符合條件1,2,在某些情況下仍舊會造成執行緒執行慢

解決方法

  • 不使用阻塞佇列,即使用同步佇列java.util.concurrent.SynchronousQueue
  • 父任務和子任務使用不同的執行緒池
  • 控制併發低於共享執行緒池的核心執行緒數(僅在無法使用上述方案時使用)