最近上線的某個項目突然開始偶現線程拒絕。
背景
內部有個框架主要用于做最終一致性,通過分布式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,異常已沒有出現,暫定為已處理。
簡單的問題也容易一葉障目。