淺析Java中的執行緒池

pedro7發表於2022-01-22

Java中的執行緒池

幾乎所有需要非同步或併發執行任務的程式都可以使用執行緒池,開發過程中合理使用執行緒池能夠帶來以下三個好處:

  • 降低資源消耗
  • 提高響應速度
  • 提高執行緒的可管理性

1. 執行緒池的實現原理

當我們提交一個新任務到執行緒池時,執行緒池的處理流程如下:

image-20220122121030945 image-20220122121222192

其中,任何建立新執行緒的操作都需要獲取全域性鎖。

ThreadPoolExecutor採取上述步驟的設計思路,是為了在執行execute()方法時,儘可能地避免獲取全域性鎖。在ThreadPoolExecutor完成預熱後,幾乎所有的execute()方法都是執行步驟二,而步驟二不需要獲取全域性鎖。

工作執行緒在完成當前的任務後,會自己到工作佇列中迴圈獲取任務來執行:

image-20220122121750261

2. 執行緒池的使用

2.1 建立執行緒池

我們通過ThreadPoolExecutor來建立執行緒池。

new ThreadPoolExecutor(corePoolSize // 核心執行緒池大小
                      ,maximumPoolSize // 執行緒池大小
                      ,keepAliveTime // 最長存活時間
                      ,milliseconds // 時間單位
                      ,runnableTaskQueue // 工作佇列
                      ,handler) // 飽和策略
  • corePoolSize 引數為執行緒池基本大小,即核心執行緒池的最大容量。當核心執行緒池未滿時,只要收到新任務都會建立一個新的執行緒。當核心執行緒池滿且無空閒工作執行緒時,會將任務存到任務佇列中。

  • **maximumPoolSize **引數為執行緒池最大數量,即執行緒池允許建立的最大執行緒數。如果任務佇列滿了,將會建立新執行緒來執行任務,直到執行緒數量達到該最大數量。值得注意的是如果使用了無界的任務佇列,那麼這個引數就沒有效果。

  • keepAliveTime 引數為空閒執行緒存活時間。

  • **TimeUnit **引數為執行緒活動保持時間的單位。

  • **workQueue **引數為任務佇列,它是一個用於儲存等待執行的任務的阻塞佇列,可以選擇以下幾個。

    佇列 底層 是否有界 其他
    ArrayBlockingQueue 陣列· 有界 FIFO
    LinkedBlockingQueue 連結串列 無界 FIFO
    SynchronousQueue 不儲存元素 有界 每個插入操作都必須等到另一個執行緒呼叫移除操作
    PriorityBlockingQueue 陣列 無界 有優先順序
  • ThreadFactor引數為執行緒工廠。可以通過執行緒工廠為每個建立出來的執行緒設定更有意義的名字。

  • RejectedExecutionHandler引數為飽和策略。當佇列和執行緒池都滿了,我們需要使用飽和策略來處理新任務。一般有以下四種策略。

    1. AbortPolicy:直接丟擲異常。
    2. CallerRunsPolicy:只用呼叫者所線上程來執行任務。
    3. DiscardOldestPolicy:丟棄佇列裡最近的一個任務,並執行當前任務。
    4. DiscardPolicy:不處理,丟掉。
    5. 當然,也可以通過RejectedExecutionHandler介面自定義策略,比如記錄日誌或者持久化儲存不能處理的任務。
淺析Java中的執行緒池

為什麼要求用 ThreadPoolExecutor 建立執行緒池

否則有資源耗盡的風險,Executors返回的執行緒池物件弊端如下:

  • FixedThreadPoolSingleThreadPool:允許的請求佇列長度為Integer.MAX_VALUE,可能會堆積大量請求,導致 OOM。
  • CachedThreadPoolScheduledThreadPool:允許的建立執行緒數量為Integer.MAx_VALUE,可能會建立大量執行緒,導致 OOM。

2.2 向執行緒池提交任務

可以用execute()submit()兩個方法向執行緒池提交任務,前者提交不需要返回值的任務,後者提交需要返回值的任務。

2.3 關閉執行緒池

使用shutdown()shutdownNow()方法來關閉執行緒池。原理是遍歷執行緒池中的工作執行緒,逐個呼叫interrupt()方法。

shutdownNow()會將執行緒池狀態設定為STOP,並嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任務的列表。而shutdown()只是將執行緒池狀態設定為SHUTDOWN狀態,然後中斷所有沒有正在執行任務的執行緒。

通常呼叫shutdown()方法,如果任務不一定要執行完,就呼叫shutdownNow()

3. 合理配置執行緒池

我們需要通過任務的特性,來分析合理的執行緒池配置方式:

  • 任務的性質:CPU 密集型任務、IO 密集型任務和混合型任務
    • CPU 密集型任務配置儘可能少的執行緒,如配置 CPU 核數+ 1 個執行緒。
    • IO 密集型人物應配置儘可能多的執行緒,如配置 2 * cpu 核數個執行緒。
    • 混合型任務儘量拆分
  • 任務優先順序:高、中、低
    • 優先順序不同的任務可以使用優先順序佇列PriorityBlockingQueue來處理。
  • 任務執行時間:長、中、短
    • 執行時間不同的任務交給不同規模的執行緒池處理。
  • 任務依賴性:是否以來其他系統資源,如資料庫資源
    • 依賴資料庫連線池的任務,因為執行緒提交 SQL 後需要等待資料庫返回結果,等待時間越長則 CPU 空閒時間越長,那麼執行緒數應該設定得越大,才能更好地使用 CPU。

相關文章