震驚,程序運行一半就不運行了
近期,我們的項目在生產環境中運行時頻繁出現一個難以理解的Bug。這個問題頗為有趣,因此我決定在此記錄下整個排查過程。
首先,讓我模擬一下出問題的代碼:
XController.java
@Resource private XService xService; @GetMapping("/method1") public void method1(){ System.out.println("a"); xService.method2(); }
XService.java
@Async public void method2() { System.out.println("b"); }
如上代碼所示,邏輯相當簡單。在Controller中的`method1`里,我們首先打印一個“a”,然后調用Service的`method2`,在`method2`中打印一個“b”。
按照常理,每次調用`method1`都應該依次打印“a”和“b”。大多數人可能都會這樣認為,也確信結果必然如此。
然而,事實日志卻類似這樣:
2024-07-01 [nio-9099-exec-2] : a 2024-07-01 [ myThread-1] : b 2024-07-01 [nio-9099-exec-2] : a 2024-07-01 [ myThread-1] : b 2024-07-01 [nio-9099-exec-2] : a 2024-07-01 [nio-9099-exec-2] : a 2024-07-01 [nio-9099-exec-2] : a 2024-07-01 [nio-9099-exec-2] : a 2024-07-01 [nio-9099-exec-2] : a
如上所示,有時只打印了“a”,而沒有打印“b”,并且沒有任何異常日志。
這個現象超出了我們的認知,一時間讓人感到困惑。程序為何運行一半就停止了?難道多年的認知要被推翻,全世界都錯了嗎?
仔細思考后,作為一個擁有多年CRUD經驗的程序員,我認為問題應該還是出在我自己身上。那么,問題究竟在哪里呢?
通過對比常規代碼與這段代碼的區別,我發現這段代碼唯一特殊的地方在于使用了`@Async`注解。
另外,這部分代碼近期才開始顯現出問題,難道是其他地方的改動影響到了這里?
有兩個排查方向:一是調查`@Async`的使用情況;二是檢查近期的代碼更改。
眾所周知,`@Async`的作用是讓系統在新的線程中執行`method2`。但為什么有時不執行呢?難道是系統沒有開啟新線程?或者是線程不夠用了?
通常情況下,`@Async`使用的是Spring默認的線程池,而這個線程池的大小幾乎是無限的。我們系統的訪問量并未達到這個級別。
會不會是`@Async`使用了其他的線程池?為了核實這一點,我查看了日志文件,發現在打印“b”的那行日志前面有線程名“myThread-1”。這個線程名顯然是自定義的,并非Spring自帶的。
進一步在代碼中全局搜索“myThread”關鍵字,果然發現了自定義線程池的地方。經過檢查代碼提交記錄,確認是其他同事最近提交的代碼引起的問題。
再來看自定義線程池的配置:
executor.setMaxPoolSize(1); executor.setQueueCapacity(50); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
線程池中只有一個線程,隊列容量為50。這意味著同時只能有50個任務排隊等待執行。拒絕策略采用了`DiscardPolicy`,該策略會在隊列已滿時直接拋棄新任務,且不拋出異常。
復盤系統實際運行過程:當`method1`執行到`method2`時,會使用自定義線程池中的唯一一個線程`myThread-1`來執行。如果`myThread-1`已有任務在執行,新任務會進入隊列排隊。若任務過多超過50個,新任務就會被拋棄,不再執行。
由于任務被拋棄,這就解釋了為何沒有打印“b”。同時,由于采用了`DiscardPolicy`策略,因此沒有異常日志產生。
知道了問題的原因,解決方案就變得清晰了。有兩種解決方法:
1. 移除自定義線程池,回歸使用默認線程池(盡管默認線程池存在無限隊列問題,但我們的項目負載不會那么大)。
2. 調整自定義線程池的配置,增加線程數至8,隊列容量擴大到幾千,并將拒絕策略改為拋出異常的策略。

浙公網安備 33010602011771號