【踩坑指南】執行緒池使用不當的五個坑
來源:程式設計師wayn
執行緒池是 Java 多執行緒程式設計中的一個重要概念,它可以有效地管理和複用執行緒資源,提高系統的效能和穩定性。但是執行緒池的使用也有一些注意事項和常見的錯誤,如果不小心,就可能會導致一些嚴重的問題,比如記憶體洩漏、死鎖、效能下降等。最後文末還有免費紅包封面可以領取,回饋給各位讀者朋友。
本文將介紹執行緒池使用不當的五個坑,以及如何避免和解決它們,大綱如下,
坑一:執行緒池中異常消失
執行緒池執行方法時要新增異常處理,這是一個老生常談的問題,可是直到最近我都有同事還在犯這個錯誤,所以我還是要講一下,不過我還提到了一種優雅的執行緒池全域性異常處理的方法,大家可以往下看。
問題原因
@Test
public void test() throws Exception {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5,
10,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100000));
Future<Integer> submit = threadPoolExecutor.execute(() -> {
int i = 1 / 0; // 發生異常
return i;
});
}
如上程式碼,線上程池執行任務時,沒有新增異常處理。導致任務內部發生異常時,內部錯誤無法被記錄下來。
解決方法
線上程池執行任務方法內新增 try/catch 處理,程式碼如下,
@Test
public void test() throws Exception {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5,
10,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100000));
Future<Integer> submit = threadPoolExecutor.execute(() -> {
try {
int i = 1 / 0;
return i;
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
});
}
優雅的進行執行緒池異常處理
當執行緒池呼叫任務方法很多時,那麼每個執行緒池任務執行的方法內都要新增 try/catch 處理,這就不優雅了,其實 ThreadPoolExecutor 執行緒池類支援傳入 ThreadFactory 引數用於自定義執行緒工廠,這樣我們在建立執行緒時,就可以指定 setUncaughtExceptionHandler 異常處理方法。
這樣就可以做到全域性處理異常了,程式碼如下,
ThreadFactory threadFactory = r -> {
Thread thread = new Thread(r);
thread.setUncaughtExceptionHandler((t, e) -> {
// 記錄執行緒異常
log.error(e.getMessage(), e);
});
return thread;
};
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5,
10,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100000));
threadPoolExecutor.execute(() -> {
log.info("---------------------");
int i = 1 / 0;
});
不過要注意的是上面 setUncaughtExceptionHandler 方法只能針對執行緒池的 execute 方法來全域性處理異常。對於執行緒池的 submit 方法是無法處理的。
坑二:拒絕策略設定錯誤導致介面超時
在 Java 中,執行緒池拒絕策略可以說一個常見八股文問題。大家雖然都記住了執行緒池有四種決絕策略,可是實際程式碼編寫中,我發現大多數人都只會用 CallerRunsPolicy 策略(由呼叫執行緒處理任務)。我吃過這個虧,因此也拿出來講講。
問題原因
曾經有一個線上業務介面使用了執行緒池進行第三方介面呼叫,執行緒池配置裡的拒絕策略採用的是 CallerRunsPolicy。示例程式碼如下,
// 某個線上執行緒池配置如下
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
50, // 最小核心執行緒數
50, // 最大執行緒數,當佇列滿時,能建立的最大執行緒數
60L, TimeUnit.SECONDS, // 空閒執行緒超過核心執行緒時,回收該執行緒的最大等待時間
new LinkedBlockingQueue<>(5000), // 阻塞佇列大小,當核心執行緒使用滿時,新的執行緒會放進佇列
new CustomizableThreadFactory("task"), // 自定義執行緒名
new ThreadPoolExecutor.CallerRunsPolicy() // 執行緒執行的拒絕策略
);
threadPoolExecutor.execute(() -> {
// 呼叫第三方介面
...
});
在第三方介面異常的情況下,執行緒池任務呼叫第三方介面一直超時,導致核心執行緒數、最大執行緒數堆積被佔滿、阻塞佇列也被佔滿的情況下,也就會執行拒絕策略,但是由於使用的是 CallerRunsPolicy 策略,導致執行緒任務直接由我們的業務執行緒來執行。
因為第三方介面異常,所以業務執行緒執行也會繼繼續超時,線上服務採用的 Tomcat 容器,最終也就導致 Tomcat 的最大執行緒數也被佔滿,進而無法繼續向外提供服務。
解決方法
首先我們要考慮業務介面的可用性,就算執行緒池任務被丟棄,也不應該影響業務介面。
在業務介面穩定性得到保證的情況下,在考慮到執行緒池任務的重要性,不是很重要的話,可以使用 DiscardPolicy 策略直接丟棄,要是很重要,可以考慮使用訊息佇列來替換執行緒池。
坑三:重複建立執行緒池導致記憶體溢位
不知道大家有沒有犯過這個問題,不過我確實犯過,歸根結底還是寫程式碼前,沒有思考好業務邏輯,直接動手,寫一步算一步 😂。所以說寫程式碼的前的一些邏輯梳理、拆分、程式碼設計很重要。
問題原因
這個問題的原因很簡單,就是在一個方法內重複建立了執行緒池,在執行完之後卻沒有關閉。比較經典的就是在定時任務內使用執行緒池時有可能犯這個問題,示例程式碼如下,
@XxlJob("test")
public void test() throws Exception {
// 某個線上執行緒池配置如下
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
50, // 最小核心執行緒數
50, // 最大執行緒數,當佇列滿時,能建立的最大執行緒數
60L, TimeUnit.SECONDS, // 空閒執行緒超過核心執行緒時,回收該執行緒的最大等待時間
new LinkedBlockingQueue<>(5000), // 阻塞佇列大小,當核心執行緒使用滿時,新的執行緒會放進佇列
new CustomizableThreadFactory("task"), // 自定義執行緒名
new ThreadPoolExecutor.CallerRunsPolicy() // 執行緒執行的拒絕策略
);
threadPoolExecutor.execute(() -> {
// 任務邏輯
...
});
}
當我們在定時任務中想使用執行緒池來縮短任務執行時間時,千萬要注意別再任務內建立了執行緒池,一旦犯了,基本都會在程式執行一段時間後發現程式突然間就掛了,留下了一堆記憶體 dump 報錯的檔案 😂。
解決方法
使用執行緒池單例,切勿重複建立執行緒池。示例程式碼如下,
// 某個線上執行緒池配置如下
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
50, // 最小核心執行緒數
50, // 最大執行緒數,當佇列滿時,能建立的最大執行緒數
60L, TimeUnit.SECONDS, // 空閒執行緒超過核心執行緒時,回收該執行緒的最大等待時間
new LinkedBlockingQueue<>(5000), // 阻塞佇列大小,當核心執行緒使用滿時,新的執行緒會放進佇列
new CustomizableThreadFactory("task"), // 自定義執行緒名
new ThreadPoolExecutor.CallerRunsPolicy() // 執行緒執行的拒絕策略
);
@XxlJob("test")
public void test() throws Exception {
threadPoolExecutor.execute(() -> {
// 任務邏輯
// ...
});
}
坑四:共用執行緒池執行不同型別任務導致效率低下
有時候,我們可能會想要節省執行緒資源,把不同型別的任務都放到同一個執行緒池中執行,比如主要的業務邏輯和次要的日誌記錄、監控等。這看起來很合理,但是實際上,這樣做可能會導致一個任務影響另一個任務,甚至導致死鎖的問題。
問題原因
問題的原因是,不同型別的任務可能有不同的執行時間、優先順序、依賴關係等,如果放到同一個執行緒池中,就可能會出現以下幾種情況:
如果一個任務執行時間過長,或者出現異常,那麼它就會佔用執行緒池中的一個執行緒,導致其他任務無法及時得到執行,影響系統的吞吐量和響應時間。 如果一個任務的優先順序較低,或者不是很重要,那麼它就可能搶佔執行緒池中的一個執行緒,導致其他任務無法及時得到執行,影響系統的可用性和正確性。 如果一個任務依賴於另一個任務的結果,或者需要等待另一個任務的完成,那麼它就可能造成執行緒池中的一個執行緒被阻塞,導致其他任務無法及時得到執行,甚至導致死鎖的問題。
解決方法
解決方法也很簡單,就是使用不同的執行緒池來執行不同型別的任務,根據任務的特點和重要性來分配執行緒資源,避免一個任務影響另一個任務。具體來說,有以下幾個建議:
對於主要的業務邏輯,使用一個專門的執行緒池,根據業務的併發度和響應時間,設定合適的執行緒池引數,保證業務的正常執行和高效處理。 對於次要的日誌記錄、監控等,使用一個單獨的執行緒池,根據任務的頻率和重要性,設定合適的執行緒池引數,保證任務的非同步執行和不影響主業務。 對於有依賴關係的任務,使用一個單獨的執行緒池,根據任務的數量和複雜度,設定合適的執行緒池引數,保證任務的有序執行和不造成死鎖。
坑五:使用 ThreadLocal 和執行緒池的不相容問題
ThreadLocal 是 Java 提供的一個工具類,它可以讓每個執行緒擁有自己的變數副本,從而實現執行緒間的資料隔離,比如儲存一些執行緒相關的上下文資訊,如使用者 ID、請求 ID 等。這看起來很有用,但是如果和執行緒池一起使用,就可能會出現一些意想不到的問題,比如資料錯亂、記憶體洩漏等。
問題原因
問題的原因是,ThreadLocal 和執行緒池的設計理念是相悖的,ThreadLocal 是基於執行緒的,而執行緒池是基於任務的。具體來說,有以下幾個問題:
ThreadLocal 的變數是繫結線上程上的,而執行緒池的執行緒是可以複用的,如果一個執行緒執行完一個任務後,沒有清理 ThreadLocal 的變數,那麼這個變數就會被下一個執行的任務繼承,導致資料錯亂的問題。 ThreadLocal 的變數是儲存在 Thread 類的一個 ThreadLocalMap 型別的屬性中的,這個屬性是一個弱引用的 Map,它的鍵是 ThreadLocal 物件,而值是變數的副本。如果 ThreadLocal 物件被回收,那麼它的鍵就會失效,但是值還會保留在 Map 中,導致記憶體洩漏的問題。
解決方法
解決方法也很簡單,就是在使用 ThreadLocal 和執行緒池的時候,注意以下幾點:
在使用 ThreadLocal 的變數之前,要確保為每個執行緒設定了正確的初始值,避免使用上一個任務的遺留值。 在使用 ThreadLocal 的變數之後,要及時地清理 ThreadLocal 的變數,避免變數的副本被下一個執行的任務繼承,或者佔用記憶體空間,導致記憶體洩漏的問題。可以使用 try-finally 語句,或者使用 Java 8 提供的 AutoCloseable 介面,來實現自動清理的功能。 在使用 ThreadLocal 的時候,要注意執行緒池的大小和任務的數量,避免建立過多的 ThreadLocal 物件和變數的副本,導致記憶體佔用過大的問題。可以使用一些工具,如 VisualVM,來監控執行緒池和 ThreadLocal 的狀態,及時發現和解決問題。
總結
本文給大家介紹了執行緒池使用不當的五個坑,分別是執行緒池中異常消失、執行緒池決絕策略設定錯誤、重複建立執行緒池導致記憶體溢位、使用同一個執行緒池執行不同型別的任務、使用 ThreadLocal 和執行緒池的不相容問題,以及它們的問題原因和解決方法。希望這些內容對大家有幫助。
來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70024924/viewspace-3006216/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 踩坑 Spring Cloud Hystrix 執行緒池佇列配置SpringCloud執行緒佇列
- Java併發 之 執行緒池系列 (1) 讓多執行緒不再坑爹的執行緒池Java執行緒
- 執行緒池運用不當的一次線上事故執行緒
- phppresentation 踩坑指南PHP
- mpvue“踩坑”指南Vue
- 案例解析:執行緒池使用不當導致的系統崩潰執行緒
- ?踩坑指南——onnx系列
- 填個坑!再談執行緒池動態調整那點事。執行緒
- Druid監控踩坑指南UI
- iOS私有Pod, 指南+踩坑iOS
- 細數執行緒池五大坑,一不小心線上就崩了執行緒
- 又踩坑了!BigDecimal使用的5個坑!Decimal
- 執行緒池的五種狀態及建立執行緒池的幾種方式執行緒
- 聊聊併發(五)——執行緒池執行緒
- 用Java執行Python:Jython踩坑筆記JavaPython筆記
- 沒想到,這麼簡單的執行緒池用法,深藏這麼多坑!執行緒
- Hexo6 升級踩坑指南Hexo
- 微信小程式踩坑指南【一】微信小程式
- mybatis plus +springboot +jsp整合踩坑指南MyBatisSpring BootJS
- 當年用httpclient時踩過的那些坑HTTPclient
- Jenkins踩坑之旅:nohup後臺執行shell命令Jenkins
- JavaScript 中 this 的執行機制及爬坑指南JavaScript
- JavaScript中this的執行機制及爬坑指南JavaScript
- React兩個bug踩坑React
- Java執行緒池二:執行緒池原理Java執行緒
- 遊戲人避坑指南——怎樣才能減少踩坑的頻率?遊戲
- golang的踩坑Golang
- 小程式踩坑填坑
- 多執行緒【執行緒池】執行緒
- 執行緒和執行緒池執行緒
- 執行緒 執行緒池 Task執行緒
- Monitor Ctrl-Break執行緒,有點坑執行緒
- 分散式配置nacos搭建踩坑指南(下)分散式
- GAMES101 作業7 踩坑指南GAM
- 踩坑指南:入門OpenTenBase之部署篇
- sonarqube配置全指南,sonarqube踩坑記錄
- SM 國密演算法踩坑指南演算法
- Flutter開發環境搭建-踩坑指南Flutter開發環境