線程池queueCapacity踩坑了

最近上線的某個項目突然開始偶現線程拒絕。

背景

內部有個框架主要用于做最終一致性,通過分布式JOB+DB的方式來處理一些存在分布式事務,比如DB+MQ。方案的思路是運行時先往數據庫落一個任務,然后異步執行任務。

通過定時任務異步的撈取任務來檢查狀態決定是否重試,以此達到最終一致性。

任務的處理通過線程池來控制。

排查

先看線程池的參數設置:

  • 核心線程數 x
  • 隊列長度為 x/2
  • 最大線程數 x + x/2
  • keepAliveSeconds 10h

粗略一看,感覺對于當前的任務處理沒什么問題,但是其實埋藏著一些風險。

檢查是否有運行阻塞的任務

通過arthas trace跟蹤,得到的結果非常正常。

看線程堆棧

通過jstask 打印出線程堆棧來查看,發現所有的線程都處于阻塞,等待任務的狀態。(線程是x + x/2)

java.lang.Thread.State: TIMED_WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000821ce058> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078)
        at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1073)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)
--
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000842cf2b8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

這明顯跟現在的任務運行不符合,現在的任務是每次定時任務觸發時,會觸發x個任務,但是會有一定數量的異常,具體多少個沒有細算。

下意識的假定,現在所有的線程都處于等待任務的狀態,說明隊列是空的,那么此時任務至少應該是x+x/2+x+x/2 的數量才會開始拋出拒絕狀態。

因為這個錯誤的假定,耗費了非常多的時間去排查其他問題。后來反復看堆棧,才摸到了思路,源自于parking to wait for這個提示。

如果線程池是剛創建出來的,那么毫無疑問,假定是正確的,可是運行了一段時間的線程池,如果沒有及時回收的話,線程會處于阻塞,等待隊列任務的狀態,看線程池的提交任務的代碼:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) { // 如果線程數量小于核心線程數
            if (addWorker(command, true)) // 創建工作線程,該任務為此工作線程的第一個任務執行
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) { // 添加到隊列
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false)) // 嘗試創建工作線程,false表示已最大線程數來作為條件
            reject(command);
    }

所以當工作線程都存在時,任務首先要做的是入隊列。隊列為阻塞隊列:

protected BlockingQueue<Runnable> createQueue(int queueCapacity) {
        return (BlockingQueue)(queueCapacity > 0 ? new LinkedBlockingQueue(queueCapacity) : new SynchronousQueue());
}

offer方法都有啥,核心包括入隊列,還有喚醒阻塞在隊列上的線程,而線程在運行時,從隊列中poll任務時,需要獲取鎖。

意味著,不是所有的任務進去就能立刻被線程消費,而回去看隊列的值queueCapacity = x/2,顯而易見,每次進來x個任務,隊列肯定放不下,于是就會看到每次都有一定數量的任務被拒絕。

設置隊列值queueCapacity = 2x,異常已沒有出現,暫定為已處理。

簡單的問題也容易一葉障目。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容