JUC-ThreadPoolExecutor線程池解析與BlockingQueue的三種實現




ThreadPoolExecutor介紹

ThreadPoolExecutor的完整構造方法的簽名如下

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


  1. corePoolSize(線程池的基本大小):當提交一個任務到線程池時,線程池會創建一個線程來執行任務,即使其他空閒的基本線程能夠執行新任務也會創建線程,等到需要執行的任務數大於線程池基本大小時就不再創建。如果調用了線程池的prestartAllCoreThreads方法,線程池會提前創建並啓動所有基本線程。
  2. workQueue任務隊列):用於保存等待執行的任務的阻塞隊列。可以選擇以下幾個阻塞隊列。

    1. ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。
    2. LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列
    3. SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。
    4. PriorityBlockingQueue:一個具有優先級的無限阻塞隊列
  3. maximumPoolSize(線程池最大大小):線程池允許創建的最大線程數。如果隊列滿了,並且已創建的線程數小於最大線程數,則線程池會再創建新的線程執行任務。值得注意的是如果使用了無界的任務隊列這個參數就沒什麼效果。

  4. ThreadFactory:用於設置創建線程的工廠,可以通過線程工廠給每個創建出來的線程做些更有意義的事情,比如設置daemon和優先級等等
  5. RejectedExecutionHandler(飽和策略):當隊列和線程池都滿了,說明線程池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略默認情況下是AbortPolicy,表示無法處理新任務時拋出異常。以下是JDK1.5提供的四種策略。
    1. AbortPolicy:直接拋出異常。
    2. CallerRunsPolicy:只用調用者所在線程來運行任務。
    3. DiscardOldestPolicy:丟棄隊列裏最近的一個任務,並執行當前任務。
    4. DiscardPolicy:不處理,丟棄掉。
    5. 也可以根據應用場景需要來實現RejectedExecutionHandler接口自定義策略。如記錄日誌或持久化不能處理的任務。
  6. keepAliveTime(線程活動保持時間):線程池的工作線程空閒後,保持存活的時間。所以如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高線程的利用率。
  7. TimeUnit(線程活動保持時間的單位):可選的單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

根據上面的描述,我相信我們能夠在熟悉參數的情況下自定義自己的線程池,但是我們發現在jdk幫助文檔裏面有這樣一句話

強烈建議程序員使用較爲方便的 Executors 工廠方法 Executors.newCachedThreadPool()(無界線程池,可以進行自動線程回收)、Executors.newFixedThreadPool(int)(固定大小線程池)和Executors.newSingleThreadExecutor()(單個後臺線程),它們均爲大多數使用場景預定義了設置。

線程池的工作方式

  1. 如果運行的線程少於 corePoolSize,則 Executor 始終首選添加新的線程,而不進行排隊。(什麼意思?如果當前運行的線程小於corePoolSize,則任務根本不會存放,添加到queue中
  2. 如果運行的線程等於或多於 corePoolSize,則 Executor 始終首選將請求加入隊列,而不添加新的線程
  3. 如果無法將請求加入隊列(隊列已滿),則創建新的線程,除非創建此線程超出 maximumPoolSize,如果超過,在這種情況下,新的任務將被拒絕。

那麼我們可以發現,隊列在線程池中是非常重要的角色,那麼Executors就是根據不同的隊列實現了功能不同的線程池,下面我們來看看

Executors包含的常用線程池

1.ExecutorService newFixedThreadPool(int nThreads):固定大小線程池。

public static ExecutorService newFixedThreadPool(int nThreads) {  
        return new ThreadPoolExecutor(nThreads, nThreads,  
                                      0L, TimeUnit.MILLISECONDS, 
                                      new LinkedBlockingQueue<Runnable>());  
    } 


我們可以發現,coresize和maxsize相同,超時時間爲0,隊列用的LinkedBlockingQueue無界的FIFO隊列,這表示什麼,很明顯,這個線程池始終只有<size的線程在運行,同時超時時間爲0,線程運行完後就關閉,而不會再等待超時時間,如果隊列裏面有線程任務的話就從隊列裏面取出線程,然後開啓一個新的線程開始執行

2.ExecutorService newCachedThreadPool():無界線程池

public static ExecutorService newCachedThreadPool() {  
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,  
                                      60L, TimeUnit.SECONDS,  
                                      new SynchronousQueue<Runnable>());  
    } 

SynchronousQueue隊列,一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作。所以,當我們提交第一個任務的時候,是加入不了隊列的,這就滿足了,一個線程池條件“當無法加入隊列的時候,且任務沒有達到maxsize時,我們將新開啓一個線程任務”。所以我們的maxsize是big big。時間是60s,當一個線程沒有任務執行會暫時保存60s超時時間,如果沒有的新的任務的話,會從cache中remove掉。

3.Executors.newSingleThreadExecutor();大小爲1的固定線程池,這個其實就是newFixedThreadPool(1).關注newFixedThreadPool的用法就行

排隊策略

排隊有三種通用策略:
1. 直接提交。工作隊列的默認選項是 SynchronousQueue,它將任務直接提交給線程而不保持它們。在此,如果不存在可用於立即運行任務的線程,則試圖把任務加入隊列將失敗,因此會構造一個新的線程。此策略可以避免在處理可能具有內部依賴性的請求集時出現鎖。直接提交通常要求無界 maximumPoolSizes 以避免拒絕新提交的任務。當命令以超過隊列所能處理的平均數連續到達時,此策略允許無界線程具有增長的可能性。
2. 無界隊列。使用無界隊列(例如,不具有預定義容量的 LinkedBlockingQueue)將導致在所有 corePoolSize 線程都忙時新任務在隊列中等待。這樣,創建的線程就不會超過 corePoolSize。(因此,maximumPoolSize 的值也就無效了。)當每個任務完全獨立於其他任務,即任務執行互不影響時,適合於使用無界隊列
3. 有界隊列。當使用有限的 maximumPoolSizes 時,有界隊列(如 ArrayBlockingQueue)有助於防止資源耗盡,但是可能較難調整和控制。隊列大小和最大池大小可能需要相互折衷:使用大型隊列和小型池可以最大限度地降低 CPU 使用率、操作系統資源和上下文切換開銷,但是可能導致人工降低吞吐量。如果任務頻繁阻塞(例如,如果它們是 I/O 邊界),則系統可能爲超過您許可的更多線程安排時間。使用小型隊列通常要求較大的池大小,CPU 使用率較高,但是可能遇到不可接受的調度開銷,這樣也會降低吞吐量。

使用直接提交策略,即SynchronousQueue。

首先SynchronousQueue是無界的,也就是說他存數任務的能力是沒有限制的,但是由於該Queue本身的特性,在某次添加元素後必須等待其他線程取走後才能繼續添加。在這裏不是核心線程便是新創建的線程,但是我們試想一樣下,下面的場景。

new ThreadPoolExecutor(  
            2, 3, 30, TimeUnit.SECONDS,   
            new SynchronousQueue<Runnable>(),   
            new RecorderThreadFactory("CookieRecorderPool"),   
            new ThreadPoolExecutor.CallerRunsPolicy());  

當核心線程已經有2個正在運行.
1. 此時繼續來了一個任務(A),根據前面介紹的“如果運行的線程等於或多於 corePoolSize,則 Executor 始終首選將請求加入隊列,而不添加新的線程。”,所以A被添加到queue中。
2. 又來了一個任務(B),且核心2個線程還沒有忙完,OK,接下來首先嚐試1中描述,但是由於使用的SynchronousQueue,所以一定無法加入進去
3. 此時便滿足了上面提到的“如果無法將請求加入隊列,則創建新的線程,除非創建此線程超出maximumPoolSize,在這種情況下,任務將被拒絕。”,所以必然會新建一個線程來運行這個任務。
4. 暫時還可以,但是如果這三個任務都還沒完成,連續來了兩個任務,第一個添加入queue中,後一個呢?queue中無法插入,而線程數達到了maximumPoolSize,所以只好執行異常策略了。

所以在使用SynchronousQueue通常要求maximumPoolSize是無界的,這樣就可以避免上述情況發生(如果希望限制就直接使用有界隊列)。對於使用SynchronousQueue的作用jdk中寫的很清楚:此策略可以避免在處理可能具有內部依賴性的請求集時出現鎖。
什麼意思?如果你的任務A1,A2有內部關聯,A1需要先運行,那麼先提交A1,再提交A2,當使用SynchronousQueue我們可以保證,A1必定先被執行,在A1麼有被執行前,A2不可能添加入queue中

使用無界隊列策略,即LinkedBlockingQueue

這個就拿newFixedThreadPool來說,根據前文提到的規則:如果運行的線程少於 corePoolSize,則 Executor 始終首選添加新的線程,而不進行排隊。那麼當任務繼續增加,會發生什麼呢?
如果無法將請求加入隊列,則創建新的線程,除非創建此線程超出 maximumPoolSize,在這種情況下,任務將被拒絕。
這裏就很有意思了,可能會出現無法加入隊列嗎?不像SynchronousQueue那樣有其自身的特點,對於無界隊列來說,總是可以加入的(資源耗盡,當然另當別論)。換句說,永遠也不會觸發產生新的線程!corePoolSize大小的線程數會一直運行,忙完當前的,就從隊列中拿任務開始運行。所以要防止任務瘋長,比如任務運行的實行比較長,而添加任務的速度遠遠超過處理任務的時間,而且還不斷增加,如果任務內存大一些,不一會兒就爆了

有界隊列,使用ArrayBlockingQueue。

個是最爲複雜的使用,所以JDK不推薦使用也有些道理。與上面的相比,最大的特點便是可以防止資源耗盡的情況發生。

new ThreadPoolExecutor(  
            2, 4, 30, TimeUnit.SECONDS,   
            new ArrayBlockingQueue<Runnable>(2),   
            new RecorderThreadFactory("CookieRecorderPool"),   
            new ThreadPoolExecutor.CallerRunsPolicy());  


假設,所有的任務都永遠無法執行完。對於首先來的A,B來說直接運行,接下來,如果來了C,D,他們會被放到queu中,如果接下來再來E,F,則增加線程運行E,F。但是如果再來任務,隊列無法再接受了,線程數也到達最大的限制了,所以就會使用拒絕策略來處理。

Summary

  1. ThreadPoolExecutor的使用還是很有技巧的。
  2. 使用無界queue可能會耗盡系統資源。
  3. 使用有界queue可能不能很好的滿足性能,需要調節線程數和queue大小
  4. 線程數自然也有開銷,所以需要根據不同應用進行調節。

通常來說對於靜態任務可以歸爲:
1. 數量大,但是執行時間很短
2. 數量小,但是執行時間較長
3. 數量又大執行時間又長
4. 除了以上特點外,任務間還有些內在關係
5. CPU密集或者IO密集型任務

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