線程池—Executors 詳解

線程池的創建分爲兩種方式:ThreadPoolExecutor 和 Executors,上一節學習了 ThreadPoolExecutor 的使用方式,本節重點來看 Executors 是如何創建線程池的。
Executors 可以創建以下六種線程池。

  • FixedThreadPool(n):創建一個數量固定的線程池,超出的任務會在隊列中等待空閒的線程,可用於控制程序的最大併發數。

  • CachedThreadPool():短時間內處理大量工作的線程池,會根據任務數量產生對應的線程,並試圖緩存線程以便重複使用,如果限制 60 秒沒被使用,則會被移除緩存。

  • SingleThreadExecutor():創建一個單線程線程池。

  • ScheduledThreadPool(n):創建一個數量固定的線程池,支持執行定時性或週期性任務。

  • SingleThreadScheduledExecutor():此線程池就是單線程的 newScheduledThreadPool。

  • WorkStealingPool(n):Java 8 新增創建線程池的方法,創建時如果不設置任何參數,則以當前機器處理器個數作爲線程個數,此線程池會並行處理任務,不能保證執行順序。

下面分別來看以上六種線程池的具體代碼使用。

FixedThreadPool 使用

創建固定個數的線程池,具體示例如下:

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
for (int i = 0; i < 3; i++) {
    fixedThreadPool.execute(() -> {
        System.out.println("CurrentTime - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

以上程序執行結果如下:

CurrentTime - 2019-06-27 20:58:58

CurrentTime - 2019-06-27 20:58:58

CurrentTime - 2019-06-27 20:58:59

根據執行結果可以看出,newFixedThreadPool(2) 確實是創建了兩個線程,在執行了一輪(2 次)之後,停了一秒,有了空閒線程,才執行第三次。

CachedThreadPool 使用

根據實際需要自動創建帶緩存功能的線程池,具體代碼如下:

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
    cachedThreadPool.execute(() -> {
        System.out.println("CurrentTime - " +
                           LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

以上程序執行結果如下:

CurrentTime - 2019-06-27 21:24:46

CurrentTime - 2019-06-27 21:24:46

CurrentTime - 2019-06-27 21:24:46

CurrentTime - 2019-06-27 21:24:46

CurrentTime - 2019-06-27 21:24:46

CurrentTime - 2019-06-27 21:24:46

CurrentTime - 2019-06-27 21:24:46

CurrentTime - 2019-06-27 21:24:46

CurrentTime - 2019-06-27 21:24:46

CurrentTime - 2019-06-27 21:24:46

根據執行結果可以看出,newCachedThreadPool 在短時間內會創建多個線程來處理對應的任務,並試圖把它們進行緩存以便重複使用。

SingleThreadExecutor 使用

創建單個線程的線程池,具體代碼如下:

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 3; i++) {
    singleThreadExecutor.execute(() -> {
        System.out.println("CurrentTime - " +
                           LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

以上程序執行結果如下:

CurrentTime - 2019-06-27 21:43:34

CurrentTime - 2019-06-27 21:43:35

CurrentTime - 2019-06-27 21:43:36

ScheduledThreadPool 使用

創建一個可以執行週期性任務的線程池,具體代碼如下:

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
scheduledThreadPool.schedule(() -> {
    System.out.println("ThreadPool:" + LocalDateTime.now());
}, 1L, TimeUnit.SECONDS);
System.out.println("CurrentTime:" + LocalDateTime.now());

以上程序執行結果如下:

CurrentTime:2019-06-27T21:54:21.881

ThreadPool:2019-06-27T21:54:22.845

根據執行結果可以看出,我們設置的 1 秒後執行的任務生效了。

SingleThreadScheduledExecutor 使用

創建一個可以執行週期性任務的單線程池,具體代碼如下:

ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
singleThreadScheduledExecutor.schedule(() -> {
    System.out.println("ThreadPool:" + LocalDateTime.now());
}, 1L, TimeUnit.SECONDS);
System.out.println("CurrentTime:" + LocalDateTime.now());
WorkStealingPool 使用

Java 8 新增的創建線程池的方式,可根據當前電腦 CPU 處理器數量生成相應個數的線程池,使用代碼如下:

ExecutorService workStealingPool = Executors.newWorkStealingPool();
for (int i = 0; i < 5; i++) {
    int finalNumber = i;
    workStealingPool.execute(() -> {
        System.out.println("I:" + finalNumber);
    });
}
Thread.sleep(5000);

以上程序執行結果如下:

I:0

I:3

I:2

I:1

I:4

根據執行結果可以看出,newWorkStealingPool 是並行處理任務的,並不能保證執行順序。

ThreadPoolExecutor VS Executors

ThreadPoolExecutor 和 Executors 都是用來創建線程池的,其中 ThreadPoolExecutor 創建線程池的方式相對傳統,而 Executors 提供了更多的線程池類型(6 種),但很不幸的消息是在實際開發中並不推薦使用 Executors 的方式來創建線程池。

無獨有偶《阿里巴巴 Java 開發手冊》中對於線程池的創建也是這樣規定的,內容如下:

線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的讀者更加明確線程池的運行規則,規避資源耗盡的風險。

說明:Executors 返回的線程池對象的弊端如下:

1)FixedThreadPool 和 SingleThreadPool:

允許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。

2)CachedThreadPool 和 ScheduledThreadPool:

允許的創建線程數量爲 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM。

OOM 是 OutOfMemoryError 的縮寫,指內存溢出的意思。

爲什麼不允許使用 Executors?

我們先來看一個簡單的例子:

ExecutorService maxFixedThreadPool =  Executors.newFixedThreadPool(10);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
    maxFixedThreadPool.execute(()->{
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

之後設置 JVM(Java 虛擬機)的啓動參數: -Xmx10m -Xms10m (設置 JVM 最大運行內存等於 10M)運行程序,會拋出 OOM 異常,信息如下:

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded

at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)

at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)

at xxx.main(xxx.java:127)

爲什麼 Executors 會存在 OOM 的缺陷?

通過以上代碼,找到了 FixedThreadPool 的源碼,代碼如下:

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
}

可以看到創建 FixedThreadPool 使用了 LinkedBlockingQueue 作爲任務隊列,繼續查看 LinkedBlockingQueue 的源碼就會發現問題的根源,源碼如下:

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

當使用 LinkedBlockingQueue 並沒有給它指定長度的時候,默認長度爲 Integer.MAX_VALUE,這樣就會導致程序會給線程池隊列添加超多個任務,因爲任務量太大就有造成 OOM 的風險。

相關面試題

1.以下程序會輸出什麼結果?

public static void main(String[] args) {
    ExecutorService workStealingPool = Executors.newWorkStealingPool();
    for (int i = 0; i < 5; i++) {
        int finalNumber = i;
        workStealingPool.execute(() -> {
            System.out.print(finalNumber);
        });
    }
}

A:不輸出任何結果
B:輸出 0 到 9 有序數字
C:輸出 0 到 9 無需數字
D:以上全對

答:A
題目解析:newWorkStealingPool 內部實現是 ForkJoinPool,它會隨着主程序的退出而退出,因爲主程序沒有任何休眠和等待操作,程序會一閃而過,不會執行任何信息,所以也就不會輸出任何結果。

2.Executors 能創建單線程的線程池嗎?怎麼創建?

答:Executors 可以創建單線程線程池,創建分爲兩種方式:

  • Executors.newSingleThreadExecutor():創建一個單線程線程池。

  • Executors.newSingleThreadScheduledExecutor():創建一個可以執行週期性任務的單線程池。

3.Executors 中哪個線程適合執行短時間內大量任務?

答:newCachedThreadPool() 適合處理大量短時間工作任務。它會試圖緩存線程並重用,如果沒有緩存任務就會新創建任務,如果線程的限制時間超過六十秒,則會被移除線程池,因此它比較適合短時間內處理大量任務。

4.可以執行週期性任務的線程池都有哪些?

答:可執行週期性任務的線程池有兩個,分別是:newScheduledThreadPool() 和 newSingleThreadScheduledExecutor(),其中 newSingleThreadScheduledExecutor() 是 newScheduledThreadPool() 的單線程版本。

5.JDK 8 新增了什麼線程池?有什麼特點?

答:JDK 8 新增的線程池是 newWorkStealingPool(n),如果不指定併發數(也就是不指定 n),newWorkStealingPool() 會根據當前 CPU 處理器數量生成相應個數的線程池。它的特點是並行處理任務的,不能保證任務的執行順序。

6.newFixedThreadPool 和 ThreadPoolExecutor 有什麼關係?

答:newFixedThreadPool 是 ThreadPoolExecutor 包裝,newFixedThreadPool 底層也是通過 ThreadPoolExecutor 實現的。

newFixedThreadPool 的實現源碼如下:

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}

7.單線程的線程池存在的意義是什麼?

答:單線程線程池提供了隊列功能,如果有多個任務會排隊執行,可以保證任務執行的順序性。單線程線程池也可以重複利用已有線程,減低系統創建和銷燬線程的性能開銷。

8.線程池爲什麼建議使用 ThreadPoolExecutor 創建,而非 Executors?

答:使用 ThreadPoolExecutor 能讓開發者更加明確線程池的運行規則,避免資源耗盡的風險。

Executors 返回線程池的缺點如下:

  • FixedThreadPool 和 SingleThreadPool 允許請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量請求,可能會導致內存溢出;

  • CachedThreadPool 和 ScheduledThreadPool 允許創建線程數量爲 Integer.MAX_VALUE,創建大量線程,可能會導致內存溢出。

總結

Executors 可以創建 6 種不同類型的線程池,其中 newFixedThreadPool() 適合執行單位時間內固定的任務數,newCachedThreadPool() 適合短時間內處理大量任務,newSingleThreadExecutor() 和 newSingleThreadScheduledExecutor() 爲單線程線程池,而 newSingleThreadScheduledExecutor() 可以執行週期性的任務,是 newScheduledThreadPool(n) 的單線程版本,而 newWorkStealingPool() 爲 JDK 8 新增的併發線程池,可以根據當前電腦的 CPU 處理數量生成對比數量的線程池,但它的執行爲併發執行不能保證任務的執行順序。

下一篇:ThreadLocal詳解

在公衆號菜單中可自行獲取專屬架構視頻資料,包括不限於 java架構、python系列、人工智能系列、架構系列,以及最新面試、小程序、大前端均無私奉獻,你會感謝我的哈

往期精選

分佈式數據之緩存技術,一起來揭開其神祕面紗

分佈式數據複製技術,今天就教你真正分身術

數據分佈方式之哈希與一致性哈希,我就是個神算子

分佈式存儲系統三要素,掌握這些就離成功不遠了

想要設計一個好的分佈式系統,必須搞定這個理論

分佈式通信技術之發佈訂閱,乾貨滿滿

分佈式通信技術之遠程調用:RPC

消息隊列Broker主從架構詳細設計方案,這一篇就搞定主從架構

消息中間件路由中心你會設計嗎,不會就來學學

消息隊列消息延遲解決方案,跟着做就行了

秒殺系統每秒上萬次下單請求,我們該怎麼去設計

【分佈式技術】分佈式系統調度架構之單體調度,非掌握不可

CDN加速技術,作爲開發的我們真的不需要懂嗎?

煩人的緩存穿透問題,今天教就你如何去解決

分佈式緩存高可用方案,我們都是這麼幹的

每天百萬交易的支付系統,生產環境該怎麼設置JVM堆內存大小

你的成神之路我已替你鋪好,沒鋪你來捶我

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章