面試突擊:為什么創建線程池一定要用ThreadPoolExecutor?
在 Java 語言中,并發編程都是依靠線程池完成的,而線程池的創建方式又有很多,但從大的分類來說,線程池的創建總共分為兩大類:手動方式使用 ThreadPoolExecutor 創建線程池和使用 Executors 執行器自動創建線程池。那究竟要使用哪種方式來創建線程池呢?我們今天就來詳細的聊一聊。
先說結論
在 Java 語言中,一定要使用 ThreadPoolExecutor 手動的方式來創建線程池,因為這種方式可以通過參數來控制最大任務數和拒絕策略,讓線程池的執行更加透明和可控,并且可以規避資源耗盡的風險。
OOM風險演示
假如我們使用了 Executors 執行器自動創建線程池的方式來創建線程池,那么就會存現線程溢出的風險,以 CachedThreadPool 為例我們來演示一下:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExecutorExample {
static class OOMClass {
// 創建 1MB 大小的變量(1M = 1024KB = 1024*1024Byte)
private byte[] data_byte = new byte[1 * 1024 * 1024];
}
public static void main(String[] args) throws InterruptedException {
// 使用執行器自動創建線程池
ExecutorService threadPool = Executors.newCachedThreadPool();
List<Object> list = new ArrayList<>();
// 添加任務
for (int i = 0; i < 10; i++) {
int finalI = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
// 定時添加
try {
Thread.sleep(finalI * 200);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 將 1M 對象添加到集合
OOMClass oomClass = new OOMClass();
list.add(oomClass);
System.out.println("執行任務:" + finalI);
}
});
}
}
}
第 2 步將 Idea 中 JVM 最大運行內存設置為 10M(設置此值主要是為了方便演示),如下圖所示:
以上程序的執行結果如下圖所示:
從上述結果可以看出,當線程執行了 7 次之后就開始出現 OutOfMemoryError 內存溢出的異常了。
內存溢出原因分析
想要了解內存溢出的原因,我們需要查看 CachedThreadPool 實現的細節,它的源碼如下圖所示:
構造函數的第 2 個參數被設置成了 Integer.MAX_VALUE,這個參數的含義是最大線程數,所以由于 CachedThreadPool 并不限制線程的數量,當任務數量特別多的時候,就會創建非常多的線程。而上面的 OOM 示例,每個線程至少要消耗 1M 大小的內存,加上 JDK 系統類的加載也要占用一部分的內存,所以當總的運行內存大于 10M 的時候,就出現內存溢出的問題了。
使用ThreadPoolExecutor來改進
接下來我們使用 ThreadPoolExecutor 來改進一下 OOM 的問題,我們使用 ThreadPoolExecutor 手動創建線程池的方式,創建一個最大線程數為 2,最多可存儲 2 個任務的線程池,并且設置線程池的拒絕策略為忽略新任務,這樣就能保證線程池的運行內存大小不會超過 10M 了,實現代碼如下:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/**
* ThreadPoolExecutor 演示示例
*/
public class ThreadPoolExecutorExample {
static class OOMClass {
// 創建 1MB 大小的變量(1M = 1024KB = 1024*1024Byte)
private byte[] data_byte = new byte[1 * 1024 * 1024];
}
public static void main(String[] args) throws InterruptedException {
// 手動創建線程池,最大線程數 2,最多存儲 2 個任務,其他任務會被忽略
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 2,
0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),
new ThreadPoolExecutor.DiscardPolicy()); // 拒絕策略:忽略任務
List<Object> list = new ArrayList<>();
// 添加任務
for (int i = 0; i < 10; i++) {
int finalI = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
// 定時添加
try {
Thread.sleep(finalI * 200);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 將 1m 對象添加到集合
OOMClass oomClass = new OOMClass();
list.add(oomClass);
System.out.println("執行任務:" + finalI);
}
});
}
// 關閉線程池
threadPool.shutdown();
// 檢測線程池的任務執行完
while (!threadPool.awaitTermination(3, TimeUnit.SECONDS)) {
System.out.println("線程池中還有任務在處理");
}
}
}
以上程序的執行結果如下圖所示:
從上述結果可以看出,線程池從開始執行到執行結束都沒有出現 OOM 的異常,這就是手動創建線程池的優勢。
其他創建線程池的問題
除了 CachedThreadPool 線程池之外,其他使用 Executors 自動創建線程池的方式,也存在著其他一些問題,比如 FixedThreadPool 它的實現源碼如下:
而默認情況下任務隊列 LinkedBlockingQueue 的存儲容量是 Integer.MAX_VALUE,也是趨向于無限大,如下圖所示:
這樣就也會造成,因為線程池的任務過多而導致的內存溢出問題。其他幾個使用 Executors 自動創建線程池的方式也存在此問題,這里就不一一演示了。
總結線程池的創建方式總共分為兩大類:手動使用 ThreadPoolExecutor 創建線程池和自動使用 Executors 執行器創建線程池的方式。其中使用 Executors 自動創建線程的方式,因為線程個數或者任務個數不可控,可能會導致內存溢出的風險,所以在創建線程池時,建議使用 ThreadPoolExecutor 的方式來創建。