16-JAVA線程池

線程池實現原理

當向線程池提交一個任務之後,線程池的處理流程如下:

  • 判斷是否達到核心線程數,若未達到,則直接創建新的線程處理當前傳入的任務,否則進入下個流程
  • 線程池中的工作隊列是否已滿,若未滿,則將任務丟入工作隊列中先存着等待處理,否則進入下個流程
  • 是否達到最大線程數,若未達到,則創建新的線程處理當前傳入的任務,否則交給線程池中的飽和策略進行處理。

在這裏插入圖片描述

jdk中提供了線程池的具體實現,實現類是:java.util.concurrent.ThreadPoolExecutor,主要構造方法

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize:核心線程大小,當提交一個任務到線程池時,線程池會創建一個線程來執行任務,即使有其他空閒線程可以處理任務也會創新線程,等到工作的線程數大於核心線程數時就不會在創建了。如果調用了線程池的prestartAllCoreThreads方法,線程池會提前把核心線程都創造好,並啓動
  • maximumPoolSize:線程池允許創建的最大線程數。如果隊列滿了,並且以創建的線程數小於最大線程數,則線程池會再創建新的線程執行任務。如果我們使用了無界隊列,那麼所有的任務會加入隊列,這個參數就沒有什麼效果了
  • keepAliveTime:線程池的工作線程空閒後,保持存活的時間。如果沒有任務處理了,有些線程會空閒,空閒的時間超過了這個值,會被回收掉。如果任務很多,並且每個任務的執行時間比較短,避免線程重複創建和回收,可以調大這個時間,提高線程的利用率
  • unit:keepAliveTIme的時間單位,可以選擇的單位有天、小時、分鐘、毫秒、微妙、千分之一毫秒和納秒。類型是一個枚舉java.util.concurrent.TimeUnit,這個枚舉也經常使用,有興趣的可以看一下其源碼
  • workQueue:工作隊列,用於緩存待處理任務的阻塞隊列,常見的有4種
  • threadFactory:線程池中創建線程的工廠,可以通過線程工廠給每個創建出來的線程設置更有意義的名字
  • handler:飽和策略,當線程池無法處理新來的任務了,那麼需要提供一種策略處理提交的新任務,默認有4種策略.

調用線程池的execute方法處理任務,執行execute方法的過程:

  • 判斷線程池中運行的線程數是否小於corepoolsize,是:則創建新的線程來處理任務,否:執行下一步
  • 試圖將任務添加到workQueue指定的隊列中,如果無法添加到隊列,進入下一步
  • 判斷線程池中運行的線程數是否小於maximumPoolSize,是:則新增線程處理當前傳入的任務,否:將任務傳遞給handler對象rejectedExecution方法處理

線程池中常見5種工作隊列

任務太多的時候,工作隊列用於暫時緩存待處理的任務,jdk中常見的4種阻塞隊列:

  • ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按照先進先出原則對元素進行排序
  • LinkedBlockingQueue:是一個基於鏈表結構的阻塞隊列,此隊列按照先進先出排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool使用了這個隊列。
  • SynchronousQueue :一個不存儲元素的阻塞隊列,每個插入操作必須等到另外一個線程調用移除操作,否則插入操作一直處理阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用這個隊列
  • PriorityBlockingQueue:優先級隊列,進入隊列的元素按照優先級會進行排序

4種常見飽和策略

當線程池中隊列已滿,並且線程池已達到最大線程數,線程池會將任務傳遞給飽和策略進行處理。這些策略都實現了RejectedExecutionHandler接口.

JDK中提供了4種常見的飽和策略:

  • AbortPolicy:直接拋出異常
  • CallerRunsPolicy:在當前調用者的線程中運行任務,即隨丟來的任務,由他自己去處理
  • DiscardOldestPolicy:丟棄隊列中最老的一個任務,即丟棄隊列頭部的一個任務,然後執行當前傳入的任務
  • DiscardPolicy:不處理,直接丟棄掉,方法內部爲空

合理地配置線程池

性質不同任務可以用不同規模的線程池分開處理。CPU密集型任務應該儘可能小的線程,如配置cpu數量+1個線程的線程池。由於IO密集型任務並不是一直在執行任務,不能讓cpu閒着,則應配置儘可能多的線程,如:cup數量*2。混合型的任務,如果可以拆分,將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這2個任務執行的時間相差不是太大,那麼分解後執行的吞吐量將高於串行執行的吞吐量。可以通過Runtime.getRuntime().availableProcessors()方法獲取cpu數量。優先級不同任務可以對線程池採用優先級隊列來處理,讓優先級高的先執行。

使用隊列的時候建議使用有界隊列,有界隊列增加了系統的穩定性,如果採用無界隊列,任務太多的時候可能導致系統OOM,直接讓系統宕機。

線程池中線程數量的配置

在Java Concurrency in Practice書中給出了估算線程池大小的公式:

Ncpu = CUP的數量
Ucpu = 目標CPU的使用率,0<=Ucpu<=1
W/C = 等待時間與計算時間的比例
爲保存處理器達到期望的使用率,最有的線程池的大小等於:
Nthreads = Ncpu × Ucpu × (1+W/C)

參考

第18天:JAVA線程池

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