面試突擊32:為什麼創建執行緒池一定要用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(設置此值主要是為了方便演示),如下圖所示:
image.png
以上程式的執行結果如下圖所示:
image.png
從上述結果可以看出,當執行緒執行了 7 次之後就開始出現 OutOfMemoryError 記憶體溢出的異常了。

記憶體溢出原因分析

想要了解記憶體溢出的原因,我們需要查看 CachedThreadPool 實現的細節,它的源碼如下圖所示:
image.png
構造函數的第 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("執行緒池中還有任務在處理");
        }
    }
}

以上程式的執行結果如下圖所示:
image.png
從上述結果可以看出,執行緒池從開始執行到執行結束都沒有出現 OOM 的異常,這就是手動創建執行緒池的優勢。

其他創建執行緒池的問題

除了 CachedThreadPool 執行緒池之外,其他使用 Executors 自動創建執行緒池的方式,也存在著其他一些問題,比如 FixedThreadPool 它的實現源碼如下:
image.png
而默認情況下任務隊列 LinkedBlockingQueue 的存儲容量是 Integer.MAX_VALUE,也是趨向於無限大,如下圖所示:
image.png
這樣就也會造成,因為執行緒池的任務過多而導致的記憶體溢出問題。其他幾個使用 Executors 自動創建執行緒池的方式也存在此問題,這裡就不一一演示了。

總結

執行緒池的創建方式總共分為兩大類:手動使用 ThreadPoolExecutor 創建執行緒池和自動使用 Executors 執行器創建執行緒池的方式。其中使用 Executors 自動創建執行緒的方式,因為執行緒個數或者任務個數不可控,可能會導致記憶體溢出的風險,所以在創建執行緒池時,建議使用 ThreadPoolExecutor 的方式來創建

是非審之於己,毀譽聽之於人,得失安之於數。

公眾號:Java面試真題解析

面試合集://gitee.com/mydb/interview