線程池實現原理
當向線程池提交一個任務之後,線程池的處理流程如下:
- 判斷是否達到核心線程數,若未達到,則直接創建新的線程處理當前傳入的任務,否則進入下個流程
- 線程池中的工作隊列是否已滿,若未滿,則將任務丟入工作隊列中先存着等待處理,否則進入下個流程
- 是否達到最大線程數,若未達到,則創建新的線程處理當前傳入的任務,否則交給線程池中的飽和策略進行處理。
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)