前言

Java中的線程池是一個很重要的概念,它的應用場景十分廣泛,可以被廣泛的用於高併發的處理場景。J.U.C提供的線程池:ThreadPoolExecutor類,可以幫助我們管理線程並方便地並行執行任務。因此瞭解併合理使用線程池非常重要。

本文對線程池採用 3W 的策略結合源碼進行思考逐層分析,即是什麼爲什麼怎麼做。

什麼是線程池

線程池的 本質是對任務和線程的管理 ,做到了將 任務線程 兩者解耦。線程池對任務的管理可看作生產者消費者的關係,通過阻塞隊列的存與取。 阻塞隊列緩存待執行的任務,工作線程從阻塞隊列中獲取任務 。線程池對線程的管理,是結合線程池狀態,已有線程的狀態,核心線程數和最大線程數、阻塞隊列狀態做出增加、執行任務、回收、複用等操作,體現了享元模式和池化思想。

享元模式:

主要目的是實現對象的共享,運用共享技術有效地支持大量細粒度的對象,避免大量相類似的開銷。當系統中對象多的時候可以減少內存的開銷,通常與搭配工廠模式使用。

池化思想:

在多種使用對象的策略上,主張讓使用的代價最小化。在 重新創建對象的代價 遠大於 更換狀態,複用對象的代價 的前提下,將可以複用的對象放入池中待複用,以此降低使用的代價。

爲什麼要用線程池

線程池的優點,也是它爲什麼被流行使用的原因:

  • 重用線程池中的線程,避免因爲線程的創建和銷燬帶來性能開銷。
  • 能有效控制線程池的最大併發數,能提供定時執行以及定間隔循環執行等功能。
  • 線程池還提供了一種方法來約束和管理執行一組任務時消耗的資源(包括線程),避免大量的線程之間因互相搶佔系統資源而導致的阻塞現象。
  • 可維護一些基本統計信息,比如已完成任務的數量。

主要的缺點:

  • 線程池的參數不存在完美的配置,高度依賴於開發者的經驗,使用不當容易造成線上的危機
  • 線程池執行的情況和任務類型相關性較大,IO密集型和CPU密集型的任務運行起來的情況差異非常大,業界並沒有一些成熟的經驗策略幫助開發人員參考。

怎麼用線程池

先了解線程池的相關重要概念:

Core and maximum pool sizes

核心線程數以及最大線程數,這是構造一個線程池所必需的參數。

不同的搭配會有不同效果的線程池,也是線程池判斷在運行任務前是否需創建新線程的重要依據。

ThreadFactory

線程工廠,這是構造一個線程池的參數。

提供線程的創建,如果構建線程池不指定 ThreadFactory ,則使用默認線程工廠,創建的線程默認進入同一個 ThreadGroup 和默認線程優先級。

Keep-alive times

存活時間,這是構造一個線程池的參數。

如果線程池當前有超過 corePoolSize 大小的線程,如果非核心線程的空閒時間超過了 keepAliveTime ,則被視爲可回收的多餘線程,被終止

Queuing

任務/阻塞隊列,這是構造一個線程池的參數。

不同類型的阻塞隊列可以構造出適合不同場景的線程池。最常見的四種線程池就有着不同類型的阻塞隊列。

作爲任務的緩衝停留區,線程池管理線程的機制核心之一。

生產者消費者模式的體現,生產者是往隊列裏添加元素的線程,消費者是從隊列裏拿元素的線程。阻塞隊列就是生產者存放元素的容器,而消費者也只從容器裏拿元素。

  • 在隊列爲空時,獲取元素的線程會等待隊列變爲非空再嘗試獲取
  • 當隊列滿時,存儲元素的線程會等待隊列可用再嘗試存儲

Rejected tasks

拒絕任務後的策略,這是構造一個線程池的參數

加入任務時,根據線程池當前狀態是否停止銷燬、線程數是否以及飽和,判斷是否拒絕本次任務的加入。若拒絕任務就會執行拒絕任務後的策略。默認的拒絕後的策略是拋出運行期異常 RejectedExecutionException

On-demand construction

需求到達才創建,默認情況下,即使是核心線程最初也只有在新任務到達時才創建和啓動,但是可以使用 prestartCoreThread 。如果使用非空隊列構造池,可能需要預啓動線程。

Hook methods

鉤子方法

可重寫的方法, beforeExecute(Runnable)afterExecute(Runnable,Throwable)terminated ,在執行每個任務之前和之後,線程池被完全終止後會被回調。可以用來執行特殊任務:重新初始化ThreadLocal變量、收集統計信息或添加日誌條目。

最常見最常用的線程池

Executors類提供的也是最常見的線程池種類,配置,以及它們維護的阻塞隊列類型,使用場景如下:

類型 核心線程數 最大線程數 阻塞隊列 說明/使用場景
FixedThreadPool 構造時傳入 與核心線程數相同 LinkedBlockingQueue 線程數量固定,只有核心線程並且不會被回收,沒有超時機制
CachedThreadPool 0 Integer.MAX_VALUE SynchronousQueue 線程數量不固定的線程池,只有非核心的線程,當線程都處於活動狀態時,直接創建新線程來處理新任務,否則就利用空閒的線程。處於空閒狀態超過60s的線程被回收
ScheduledThreadPool 構造時傳入 Integer.MAX_VALUE DelayedWorkQueue 非核心線程在閒置時立刻回收,主要用於執行定時任務和固定週期的重複任務
SingleThreadExecutor 1 1 LinkedBlockingQueue 只有一個核心線程,確保所有任務在同一線程中按順序執行

分析創建這四個線程池的方法的源碼,最後都來到了ThreadPoolExecutor類的ThreadPoolExecutor構造方法,由此可見ThreadPoolExecutor纔是真正的線程池。 Executors 作爲線程池工廠,提供的四種線程池是利用不同參數創建的適應不同使用場景的線程池。

//ThreadPoolExecutor.java

/**
    * @param corePoolSize 核心線程數
    * @param maximumPoolSize 最大線程數
    * @param keepAliveTime 非核心線程閒置的超時時長
    * @param unit 用於指定 keepAliveTime 參數的時間單位
    * @param 任務隊列,通過線程池的 execute 方法提交的 Runnable 對象會存儲在這個參數中
    * @param threadFactory 線程工廠,用於提供新線程
    * @param handler 任務隊列已滿或者是無法成功執行任務時調用
    */
public ThreadPoolExecutor(int corePoolSize,
                            int maximumPoolSize,
                            long keepAliveTime,
                            TimeUnit unit,
                            BlockingQueue<Runnable> workQueue,
                            ThreadFactory threadFactory,
                            RejectedExecutionHandler handler) {
    //···
}

線程池的簡單使用

以手動創建一個核心數爲5,最大線程數爲7,空閒超時爲20s,阻塞隊列爲數組實現的有界隊列的 ThreadPoolExecutor 爲例子:

ExecutorService executor = new ThreadPoolExecutor(
                5, 7, 20L, TimeUnit.SECONDS, 
                new ArrayBlockingQueue<Runnable>(8)
        );
        for(int i = 0; i < 9; i++){
            final int index = i;
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(String.valueOf(index)+ " " +Thread.currentThread().getName());
                }
            });
        }

手動創建線程池的好處

阿里巴巴Java開發手冊中使用強制標註:需通過手動創建 ThreadPoolExecutor 取代使用 Executors 提供的工廠方法。數據量併發量很大或難以把握時,應避免直接使用 Executors 提供的線程池,防止資源被耗盡

以CachedThreadPool爲例子,CachedThreadPool將空閒線程銷燬前的等待時間設置成了60s,同時阻塞隊列類型是 SynchronousQueue ,不存儲元素的隊列。 CachedThreadPool 在一定程度上能夠應對不斷突增的併發任務,但是一旦任務量遠遠大於處理量,會造成線程數量的激增和資源的消耗,容易引發OOM。

手動創建線程池可以更好規範該線程池的職責,更好地管理這個線程池,讓線程池在合適的場景下,可以用來處理適當的任務,而不是一顆隨時會被引爆的炸彈。

總結

線程池,基於池化思想,體現了享元模式,可以用來管理線程並方便地並行執行任務的工具。 本質上是對任務和線程解耦後進行管理 ,利用不同的構造參數可以構造出適合不同場景的線程池。優點是 降低資源消耗提高響應速度提高線程的可管理性可拓展性良好 。缺點是參數不易配置,出錯後易造成OOM。

篇幅問題,對線程池的設計和管理機制的分析安排在下一篇文章~

參考資料

相關文章