Java併發基礎一:線程池詳解

前言

假如沒有線程池,當存在較多的併發任務的時候,每執行一次任務,系統就要創建一個線程,任務完成後進行銷燬,一旦併發任務過多,頻繁的創建和銷燬線程將會大大降低系統的效率。線程池能夠對線程進行統一的分配,通過固定數量的線程來負責處理任務,避免了頻繁的創建和銷燬對象,使線程能夠重複的利用,執行多個任務。

線程池的優勢

1、降低資源消耗。通過重複利用已創建的線程降低線程創建、銷燬線程造成的消耗。
2、提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
3、提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配、調優和監控

Java線程池的結構

在這裏插入圖片描述
1、Executor 最頂層接口,僅有execute方法。真正的線程池接口應該是它的子接口ExecutorService
2、ExecutorService接口,主要對Executor接口補充了一些方法,例如shutdown()、submit()等方法
3、ThreadPoolExecutor ExecutorService的默認實現,作爲自定義線程池的主要類。

4、ScheduledExecutorService 用來解決任務重複執行的問題
5、ScheduledThreadPoolExecutor 繼承ThreadPoolExecutor的ScheduledExecutorService接口實現,週期性任務調度的類實現。

ThreadPoolExecutor

ThreadPoolExecutor是ExecutorService的默認實現,其功能也最爲基礎,通過對該類的源碼解析來了解整個線程池的工作機制。

ThreadPoolExecutor最完整的構造器

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {...}

corePoolSize

核心線程數,核心線程會一直存活,即使沒有任務需要處理。當線程數小於核心線程數時,即使現有的線程空閒,線程池也會優先創建新線程來處理任務,而不是直接交給現有的線程處理。核心線程在allowCoreThreadTimeout被設置爲true時會超時退出,默認情況下不會退出。

maxPoolSize

當線程數大於或等於核心線程,且任務隊列已滿時,線程池會創建新的線程,直到線程數量達到maxPoolSize。如果線程數已等於maxPoolSize,且任務隊列已滿,則已超出線程池的處理能力,線程池會拒絕處理任務而拋出異常。

keepAliveTime

當線程空閒時間達到keepAliveTime,該線程會退出,直到線程數量等於corePoolSize。如果allowCoreThreadTimeout設置爲true,則所有線程均會退出直到線程數量爲0。

unit

keepAliveTime的單位

allowCoreThreadTimeout

是否允許核心線程空閒退出,默認值爲false。

queueCapacity

任務隊列容量。從maxPoolSize的描述上可以看出,任務隊列的容量會影響到線程的變化,因此任務隊列的長度也需要恰當的設置。
threadFactory創建線程的工廠,通過自定義的線程工廠可以給每個新建的線程設置一個具有識別度的線程名。默認爲DefaultThreadFactory

workQueue

線程工作任務隊列。任務被執行前保存至工作隊列
常用的有ArrayBlockingQueue、LinkedBlockingQuene、priorityBlockingQuene等等

handler

線程池的飽和策略,當阻塞隊列滿了,且沒有空閒的工作線程,如果繼續提交任務,必須採取一種策略處理該任務,線程池提供了4種策略

ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。 
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。 
ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務

當然也可以根據應用場景實現RejectedExecutionHandler接口,自定義飽和策略,如記錄日誌或持久化存儲不能處理的任務。

線程池任務提交過程

  1. 如果此時線程池中的數量小於corePoolSize,即使線程池中的線程都處於空閒狀態,也要創建新的線程來處理被添加的任務。
  2. 如果此時線程池中的數量等於corePoolSize,但是緩衝隊列workQueue未滿,那麼任務被放入緩衝隊列。
  3. 如果此時線程池中的數量大於等於corePoolSize,緩衝隊列workQueue滿,並且線程池中的數量小於maximumPoolSize,建新的線程來處理被添加的任務。
  4. 如果此時線程池中的數量大於corePoolSize,緩衝隊列workQueue滿,並且線程池中的數量等於maximumPoolSize,那麼通過 handler所指定的策略來處理此任務。
  5. 當線程池中的線程數量大於 corePoolSize時,如果某線程空閒時間超過keepAliveTime,線程將被終止。這樣,線程池可以動態的調整池中的線程數。

線程池參數設置

想設置線程池參數,先了解系統負載概念。參數的設置跟系統的負載有直接的關係,下面爲系統負載的相關參數:
tasks,每秒需要處理的最大任務數量
tasktime,處理每個任務所需要的時間
responsetime,系統允許任務最大的響應時間,比如每個任務的響應時間不得超過2秒。
線程池參數設置

1.corePoolSize:
每個任務需要tasktime秒處理,則每個線程每鈔可處理1/tasktime個任務。系統每秒有tasks個任務需要處理,則需要的線程數爲:tasks/(1/tasktime),即taskstasktime個線程數。假設系統每秒任務數爲100-1000,每個任務耗時0.1秒,則需要1000.1至1000*0.1,即10~100個線程。那麼corePoolSize應該設置爲大於10,具體數字最好根據8020原則,即80%情況下系統每秒任務數,若系統80%的情況下第秒任務數小於200,最多時爲1000,則corePoolSize可設置爲20。

2.queueCapacity:
任務隊列的長度要根據核心線程數,以及系統對任務響應時間的要求有關。隊列長度可以設置爲(corePoolSize/tasktime)*responsetime: (20/0.1)*2=400,即隊列長度可設置爲400。

隊列長度設置過大,會導致任務響應時間過長,切忌以下寫法:LinkedBlockingQueue queue = new LinkedBlockingQueue();這實際上是將隊列長度設置爲Integer.MAX_VALUE,將會導致線程數量永遠爲corePoolSize,再也不會增加,當任務數量陡增時,任務響應時間也將隨之陡增。

3.maxPoolSize:
當系統負載達到最大值時,核心線程數已無法按時處理完所有任務,這時就需要增加線程。每秒200個任務需要20個線程,那麼當每秒達到1000個任務時,則需要(1000-queueCapacity)*(20/200),即60個線程,可將maxPoolSize設置爲60。

4.keepAliveTime:
線程數量只增加不減少也不行。當負載降低時,可減少線程數量,如果一個線程空閒時間達到keepAliveTiime,該線程就退出。默認情況下線程池最少會保持corePoolSize個線程。

5.allowCoreThreadTimeout:
默認情況下核心線程不會退出,可通過將該參數設置爲true,讓核心線程也退出。

說明: 以上關於線程數量的計算並沒有考慮CPU的情況。若結合CPU的情況,比如,當線程數量達到50時,CPU達到100%,則將maxPoolSize設置爲60也不合適,此時若系統負載長時間維持在每秒1000個任務,則超出線程池處理能力,應設法降低每個任務的處理時間(tasktime)。

線程池常見種類

Executors線程工廠類,提供如下4種線程創建種類:

  1. Executors.newCachedThreadPool();
    說明: 創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程.
    內部實現:new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue());

  2. Executors.newFixedThreadPool(int);
    說明: 創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
    內部實現:new ThreadPoolExecutor(nThreads, nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue());

  3. Executors.newSingleThreadExecutor();
    說明:創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照順序執行。
    內部實現:new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue())

  4. Executors.newScheduledThreadPool(int);
    說明:創建一個定長線程池,支持定時及週期性任務執行。
    內部實現:new ScheduledThreadPoolExecutor(corePoolSize)

【附】阿里巴巴Java開發手冊中對線程池的使用規範

【強制】創建線程或線程池時請指定有意義的線程名稱,方便出錯時回溯。
正例:
public class TimerTaskThread extends Thread {
public TimerTaskThread(){
super.setName(“TimerTaskThread”);

}
}
【強制】線程資源必須通過線程池提供,不允許在應用中自行顯式創建線程。
說明: 使用線程池的好處是減少在創建和銷燬線程上所花的時間以及系統資源的開銷,解決資
源不足的問題。如果不使用線程池,有可能造成系統創建大量同類線程而導致消耗完內存或者
“過度切換”的問題。

【強制】線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣
的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。
說明: Executors 返回的線程池對象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
2) CachedThreadPool 和 ScheduledThreadPool:
允許的創建線程數量爲 Integer.MAX_VALUE, 可能會創建大量的線程,從而導致 OOM。

參考:
https://blog.51cto.com/14230003/2418026?source=dra
https://blog.csdn.net/java001122/article/details/80390771
https://www.jianshu.com/p/326413683aaf
https://www.cnblogs.com/KingJack/p/9595621.html

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