線程池,坑中之王 !
前言
線程池是 Java 中處理多線程的強大工具,但它不僅僅是“直接用就完事”的工具。
很多小伙伴在用線程池時,因為配置不當或忽略細節(jié),踩過許多坑。
今天跟大家一起聊聊線程池中容易踩的 10 個坑,以及如何避免這些坑,希望對你會有所幫助。
1. 直接使用 Executors 創(chuàng)建線程池
許多初學者在創(chuàng)建線程池時,直接使用 Executors 提供的快捷方法:
ExecutorService executor = Executors.newFixedThreadPool(10);
問題在哪?
- 無界隊列:newFixedThreadPool 使用的隊列是 LinkedBlockingQueue,它是無界隊列,任務堆積可能會導致內存溢出。
- 線程無限增長:newCachedThreadPool 會無限創(chuàng)建線程,在任務量激增時可能耗盡系統(tǒng)資源。
示例:內存溢出的風險
ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 0; i < 1000000; i++) {
executor.submit(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
任務數遠大于線程數,導致任務無限堆積在隊列中,最終可能導致 OutOfMemoryError。
解決辦法
使用 ThreadPoolExecutor,并明確指定參數:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
4,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 有界隊列
new ThreadPoolExecutor.AbortPolicy() // 拒絕策略
);
2. 錯誤配置線程數
很多人隨意配置線程池參數,比如核心線程數 10,最大線程數 100,看起來沒問題,但這可能導致性能問題或資源浪費。
示例:錯誤配置導致的線程過載
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, // 核心線程數
100, // 最大線程數
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10)
);
for (int i = 0; i < 1000; i++) {
executor.submit(() -> {
try {
Thread.sleep(5000); // 模擬耗時任務
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
這種配置在任務激增時,會創(chuàng)建大量線程,系統(tǒng)資源被耗盡。
正確配置方式
根據任務類型選擇合理的線程數:
- CPU 密集型:線程數建議設置為 CPU 核心數 + 1。
- IO 密集型:線程數建議設置為 2 * CPU 核心數。
示例:
int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
cpuCores + 1,
cpuCores + 1,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(50)
);
3. 忽略任務隊列的選擇
任務隊列直接影響線程池的行為。如果選錯隊列類型,會帶來很多隱患。
常見隊列的坑
- 無界隊列:任務無限堆積。
- 有界隊列:隊列滿了會觸發(fā)拒絕策略。
- 優(yōu)先級隊列:容易導致高優(yōu)先級任務頻繁搶占低優(yōu)先級任務。
示例:任務堆積導致問題
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
4,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>()
);
for (int i = 0; i < 100000; i++) {
executor.submit(() -> System.out.println(Thread.currentThread().getName()));
}
改進方法:用有界隊列,避免任務無限堆積。
new ArrayBlockingQueue<>(100);
4. 忘記關閉線程池
有些小伙伴用完線程池后,忘記調用 shutdown(),導致程序無法正常退出。
示例:線程池未關閉
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> System.out.println("任務執(zhí)行中..."));
// 線程池未關閉,程序一直運行
正確關閉方式
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
5. 忽略拒絕策略
當任務隊列滿時,線程池會觸發(fā)拒絕策略,很多人不知道默認策略(AbortPolicy)會直接拋異常。
示例:任務被拒絕
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1,
1,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2),
new ThreadPoolExecutor.AbortPolicy() // 默認策略
);
for (int i = 0; i < 10; i++) {
executor.submit(() -> System.out.println("任務"));
}
執(zhí)行到第四個任務時會拋出 RejectedExecutionException。
改進:選擇合適的策略
- CallerRunsPolicy:提交任務的線程自己執(zhí)行。
- DiscardPolicy:直接丟棄新任務。
- DiscardOldestPolicy:丟棄最老的任務。
6. 任務中未處理異常
線程池中的任務拋出異常時,線程池不會直接拋出,導致很多問題被忽略。
示例:異常被忽略
executor.submit(() -> {
throw new RuntimeException("任務異常");
});
解決方法
捕獲任務內部異常:
executor.submit(() -> {
try {
throw new RuntimeException("任務異常");
} catch (Exception e) {
System.err.println("捕獲異常:" + e.getMessage());
}
});
自定義線程工廠:
ThreadFactory factory = r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((thread, e) -> {
System.err.println("線程異常:" + e.getMessage());
});
return t;
};
7. 阻塞任務占用線程池
如果線程池中的任務是阻塞的(如文件讀寫、網絡請求),核心線程會被占滿,影響性能。
示例:阻塞任務拖垮線程池
executor.submit(() -> {
Thread.sleep(10000); // 模擬阻塞任務
});
改進方法
- 減少任務的阻塞時間。
- 增加核心線程數。
- 使用異步非阻塞方式(如 NIO)。
8. 濫用線程池
線程池不是萬能的,某些場景直接使用 new Thread() 更簡單。
示例:過度使用線程池
一個簡單的短期任務:
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("執(zhí)行任務"));
executor.shutdown();
這種情況下,用線程池反而復雜。
改進方式
new Thread(() -> System.out.println("執(zhí)行任務")).start();
9. 未監(jiān)控線程池狀態(tài)
很多人用線程池后,不監(jiān)控其狀態(tài),導致任務堆積、線程耗盡的問題被忽略。
示例:監(jiān)控線程池狀態(tài)
System.out.println("核心線程數:" + executor.getCorePoolSize());
System.out.println("隊列大?。? + executor.getQueue().size());
System.out.println("已完成任務數:" + executor.getCompletedTaskCount());
結合監(jiān)控工具(如 JMX、Prometheus),實現實時監(jiān)控。
10. 動態(tài)調整線程池參數
有些人在線程池設計時忽略了參數調整的必要性,導致后期性能優(yōu)化困難。
示例:動態(tài)調整核心線程數
executor.setCorePoolSize(20);
executor.setMaximumPoolSize(50);
實時調整線程池參數,能適應業(yè)務的動態(tài)變化。
總結
線程池是強大的工具,但如果我們日常工作中用得不好也非常容易踩坑。
這篇文章通過實際代碼示例,我們可以清楚看到線程池的問題所在及改進方法。
希望這些內容能幫你避免踩坑,寫出高質量的線程池代碼!
線程池用得好,效率杠杠的;用得不好,程序天天崩!