Java 線程池導致CPU占用100%的原因及排查方法
近日,我們在線上服務中發現了一個容器的cpu使用率突然達到100%,為了保障系統的穩定性,我們首先將該容器下線,停止新的流量進入。然而,即使沒有新的請求,容器中的java進程cpu使用率依然居高不下。隨后,我們通過top命令檢查各個線程的使用情況,發現高cpu占用的線程是由線程池創建的。
這引發了我們的疑問:既然容器已經下線,為何線程池還會持續執行任務?接著,我們使用jstack命令查看各個線程的堆棧情況,發現線程池中有64個線程,其中40個線程處于等待新任務的狀態(waiting),而另外24個線程則處于可運行狀態(runnable)。具體的可運行線程堆棧信息如下:
"pool-1-thread-1" prio=10 tid=0x00007f9f5c01e800 nid=0x1a runnable [0x00007f9f54527000] java.lang.Thread.State: RUNNABLE at java.base/java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:464) at java.base/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1056) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1118) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at java.base/java.lang.Thread.run(Thread.java:834)
從堆棧信息可以看出,這些可運行的線程正在嘗試獲取任務,但實際上并沒有線程在執行任務。這非常奇怪,因為線程池中的線程在查詢是否有新任務的操作應該非常快且短暫。理論上,線程池中的線程應該要么獲取任務并執行任務中的代碼,要么因為沒有任務而陷入等待狀態(waiting),只有極少數情況下會處于可運行狀態并試圖獲取任務。不可能有40%的線程都處于這種狀態。
為了進一步了解情況,我們生成并分析了Java進程的火焰圖,發現CPU主要消耗在LinkedBlockingQueue.poll方法上,這與jstack的信息一致。我們開始懷疑可能是線程池的配置問題,導致線程不斷輪詢任務隊列而無法進入等待狀態(waiting)。經確認,我們的線程池配置了maxPoolSize為64,corePoolSize為16,當前線程池中的線程數量達到了最大值64。線程池還配置了keepAliveTime參數為60秒,這樣如果線程在60秒內沒有獲取到任務,它會等待60秒鐘,如果仍然沒有任務,線程將終止直到線程數量減少到corePoolSize。我們使用Arthas查看LinkedBlockingQueue.poll的調用情況,但并沒有發現它被頻繁調用。
在這一步,我們的思路陷入了停滯,目前只能懷疑LinkedBlockingQueue.poll方法存在某些bug,導致獲取不到任務時會陷入死循環。但考慮到這是JDK內部的代碼,這種明顯的bug應該是不太可能的。我們使用的JDK版本是zulu-17。是否有其他人遇到過類似的問題,提供一些提示和方向呢?
立即學習“Java免費學習筆記(深入)”;
根據現有的信息,我們可以推測在調用poll()方法時需要獲取takeLock。當大量線程同時調用poll()時,鎖競爭不可避免。在這個過程中,沒有獲取到鎖的線程會進行自旋等待操作,從而導致CPU占用率升高(仍然處于RUNNABLE狀態),這也會使keepAliveTime失效,無法回收線程。
我們可以查看LinkedBlockingQueue.poll方法的實現:
public E poll(long timeout, TimeUnit unit) throws InterruptedException { final E x; final int c; long nanos = unit.toNanos(timeout); final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); try { while (count.get() == 0) { if (nanos 1) notEmpty.signal(); } finally { takeLock.unlock(); } if (c == capacity) signalNotFull(); return x; }
基于以上分析,我們可以嘗試以下幾種解決方案:
- 調整線程池大小:將maxPoolSize從64調整到32,這樣可以減少鎖競爭。
- 更換隊列類型:嘗試使用ArrayBlockingQueue,這種隊列的鎖競爭開銷較小,雖然吞吐量可能會有所下降。
- 排查代碼:檢查是否有提交了死循環任務,導致線程池中的線程無法正常結束。
- 更換JDK:嘗試使用其他版本的JDK,看是否能解決問題。
- 檢查容器資源分配:確保容器的資源分配充足,避免因資源不足導致的性能問題。
以上就是<a