【高併發】不得不說的執行緒池與ThreadPoolExecutor類淺析

冰河發表於2022-04-14

大家好,我是冰河~~

今天,我們一起來簡單聊聊執行緒池中的ThreadPoolExecutor類,好了,不多說了,開始進入今天的正題。

一、拋磚引玉

既然Java中支援以多執行緒的方式來執行相應的任務,但為什麼在JDK1.5中又提供了執行緒池技術呢?這個問題大家自行腦補,多動腦,肯定沒壞處,哈哈哈。。。

說起Java中的執行緒池技術,在很多框架和非同步處理中介軟體中都有涉及,而且效能經受起了長久的考驗。可以這樣說,Java的執行緒池技術是Java最核心的技術之一,在Java的高併發領域中,Java的執行緒池技術是一個永遠繞不開的話題。既然Java的執行緒池技術這麼重要(怎麼能說是這麼重要呢?那是相當的重要,那傢伙老重要了,哈哈哈),那麼,本文我們就來簡單的說下執行緒池與ThreadPoolExecutor類。至於執行緒池中的各個技術細節和ThreadPoolExecutor的底層原理和原始碼解析,我們會在【高併發專題】專欄中進行深度解析。

引言:本文是高併發中執行緒池的開篇之作,就暫時先不深入講解,只是讓大家從整體上認識下執行緒池中最核心的類之一——ThreadPoolExecutor,關於ThreadPoolExecutor的底層原理和原始碼實現,以及執行緒池中的其他技術細節的底層原理和原始碼實現,我們會在【高併發專題】接下來的文章中,進行死磕。

二、Thread直接建立執行緒的弊端

(1)每次new Thread新建物件,效能差。
(2)執行緒缺乏統一管理,可能無限制的新建執行緒,相互競爭,有可能佔用過多系統資源導致當機或OOM。
(3)缺少更多的功能,如更多執行、定期執行、執行緒中斷。
(4)其他弊端,大家自行腦補,多動腦,沒壞處,哈哈。

三、執行緒池的好處

(1)重用存在的執行緒,減少物件建立、消亡的開銷,效能佳。
(2)可以有效控制最大併發執行緒數,提高系統資源利用率,同時可以避免過多資源競爭,避免阻塞。
(3)提供定時執行、定期執行、單執行緒、併發數控制等功能。
(4)提供支援執行緒池監控的方法,可對執行緒池的資源進行實時監控。
(5)其他好處,大家自行腦補,多動腦,沒壞處,哈哈。

四、執行緒池

1.執行緒池類結構關係

執行緒池中的一些介面和類的結構關係如下圖所示。

後文會死磕這些介面和類的底層原理和原始碼。

2.建立執行緒池常用的類——Executors

  • Executors.newCachedThreadPool:建立一個可快取的執行緒池,如果執行緒池的大小超過了需要,可以靈活回收空閒執行緒,如果沒有可回收執行緒,則新建執行緒
  • Executors.newFixedThreadPool:建立一個定長的執行緒池,可以控制執行緒的最大併發數,超出的執行緒會在佇列中等待
  • Executors.newScheduledThreadPool:建立一個定長的執行緒池,支援定時、週期性的任務執行
  • Executors.newSingleThreadExecutor: 建立一個單執行緒化的執行緒池,使用一個唯一的工作執行緒執行任務,保證所有任務按照指定順序(先入先出或者優先順序)執行
  • Executors.newSingleThreadScheduledExecutor:建立一個單執行緒化的執行緒池,支援定時、週期性的任務執行
  • Executors.newWorkStealingPool:建立一個具有並行級別的work-stealing執行緒池

3.執行緒池例項的幾種狀態

  • Running:執行狀態,能接收新提交的任務,並且也能處理阻塞佇列中的任務
  • Shutdown: 關閉狀態,不能再接收新提交的任務,但是可以處理阻塞佇列中已經儲存的任務,當執行緒池處於Running狀態時,呼叫shutdown()方法會使執行緒池進入該狀態
  • Stop: 不能接收新任務,也不能處理阻塞佇列中已經儲存的任務,會中斷正在處理任務的執行緒,如果執行緒池處於Running或Shutdown狀態,呼叫shutdownNow()方法,會使執行緒池進入該狀態
  • Tidying: 如果所有的任務都已經終止,有效執行緒數為0(阻塞佇列為空,執行緒池中的工作執行緒數量為0),執行緒池就會進入該狀態。
  • Terminated: 處於Tidying狀態的執行緒池呼叫terminated()方法,會使用執行緒池進入該狀態

注意:不需要對執行緒池的狀態做特殊的處理,執行緒池的狀態是執行緒池內部根據方法自行定義和處理的。

4.合理配置執行緒的一些建議

(1)CPU密集型任務,就需要儘量壓榨CPU,參考值可以設定為NCPU+1(CPU的數量加1)。
(2)IO密集型任務,參考值可以設定為2*NCPU(CPU數量乘以2)

五、執行緒池最核心的類之一——ThreadPoolExecutor

1.構造方法

ThreadPoolExecutor引數最多的構造方法如下:

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

其他的構造方法都是呼叫的這個構造方法來例項化物件,可以說,我們直接分析這個方法之後,其他的構造方法我們也明白是怎麼回事了!接下來,就對此構造方法進行詳細的分析。

注意:為了更加深入的分析ThreadPoolExecutor類的構造方法,會適當調整引數的順序進行解析,以便於大家更能深入的理解ThreadPoolExecutor構造方法中每個引數的作用。

上述構造方法接收如下引數進行初始化:

(1)corePoolSize:核心執行緒數量。

(2)maximumPoolSize:最大執行緒數。

(3)workQueue:阻塞佇列,儲存等待執行的任務,很重要,會對執行緒池執行過程產生重大影響。

其中,上述三個引數的關係如下所示:

  • 如果執行的執行緒數小於corePoolSize,直接建立新執行緒處理任務,即使執行緒池中的其他執行緒是空閒的。
  • 如果執行的執行緒數大於等於corePoolSize,並且小於maximumPoolSize,此時,只有當workQueue滿時,才會建立新的執行緒處理任務。
  • 如果設定的corePoolSize與maximumPoolSize相同,那麼建立的執行緒池大小是固定的,此時,如果有新任務提交,並且workQueue沒有滿時,就把請求放入到workQueue中,等待空閒的執行緒,從workQueue中取出任務進行處理。
  • 如果執行的執行緒數量大於maximumPoolSize,同時,workQueue已經滿了,會通過拒絕策略引數rejectHandler來指定處理策略。

根據上述三個引數的配置,執行緒池會對任務進行如下處理方式:

當提交一個新的任務到執行緒池時,執行緒池會根據當前執行緒池中正在執行的執行緒數量來決定該任務的處理方式。處理方式總共有三種:直接切換、使用無限佇列、使用有界佇列。

  • 直接切換常用的佇列就是SynchronousQueue。
  • 使用無限佇列就是使用基於連結串列的佇列,比如:LinkedBlockingQueue,如果使用這種方式,執行緒池中建立的最大執行緒數就是corePoolSize,此時maximumPoolSize不會起作用。當執行緒池中所有的核心執行緒都是執行狀態時,提交新任務,就會放入等待佇列中。
  • 使用有界佇列使用的是ArrayBlockingQueue,使用這種方式可以將執行緒池的最大執行緒數量限制為maximumPoolSize,可以降低資源的消耗。但是,這種方式使得執行緒池對執行緒的排程更困難,因為執行緒池和佇列的容量都是有限的了。

根據上面三個引數,我們可以簡單得出如何降低系統資源消耗的一些措施:

  • 如果想降低系統資源的消耗,包括CPU使用率,作業系統資源的消耗,上下文環境切換的開銷等,可以設定一個較大的佇列容量和較小的執行緒池容量。這樣,會降低執行緒處理任務的吞吐量。
  • 如果提交的任務經常發生阻塞,可以考慮呼叫設定最大執行緒數的方法,重新設定執行緒池最大執行緒數。如果佇列的容量設定的較小,通常需要將執行緒池的容量設定的大一些,這樣,CPU的使用率會高些。如果執行緒池的容量設定的過大,併發量就會增加,則需要考慮執行緒排程的問題,反而可能會降低處理任務的吞吐量。

接下來,我們繼續看ThreadPoolExecutor的構造方法的引數。

(4)keepAliveTime:執行緒沒有任務執行時最多保持多久時間終止
當執行緒池中的執行緒數量大於corePoolSize時,如果此時沒有新的任務提交,核心執行緒外的執行緒不會立即銷燬,需要等待,直到等待的時間超過了keepAliveTime就會終止。

(5)unit:keepAliveTime的時間單位

(6)threadFactory:執行緒工廠,用來建立執行緒
預設會提供一個預設的工廠來建立執行緒,當使用預設的工廠來建立執行緒時,會使新建立的執行緒具有相同的優先順序,並且是非守護的執行緒,同時也設定了執行緒的名稱

(7)rejectHandler:拒絕處理任務時的策略

如果workQueue阻塞佇列滿了,並且沒有空閒的執行緒池,此時,繼續提交任務,需要採取一種策略來處理這個任務。

執行緒池總共提供了四種策略:

  • 直接丟擲異常,這也是預設的策略。實現類為AbortPolicy。
  • 用呼叫者所在的執行緒來執行任務。實現類為CallerRunsPolicy。
  • 丟棄佇列中最靠前的任務並執行當前任務。實現類為DiscardOldestPolicy。
  • 直接丟棄當前任務。實現類為DiscardPolicy。

2.ThreadPoolExecutor提供的啟動和停止任務的方法

(1)execute():提交任務,交給執行緒池執行
(2)submit():提交任務,能夠返回執行結果 execute+Future
(3)shutdown():關閉執行緒池,等待任務都執行完
(4)shutdownNow():立即關閉執行緒池,不等待任務執行完

3.ThreadPoolExecutor提供的適用於監控的方法

(1)getTaskCount():執行緒池已執行和未執行的任務總數
(2)getCompletedTaskCount():已完成的任務數量
(3)getPoolSize():執行緒池當前的執行緒數量
(4)getCorePoolSize():執行緒池核心執行緒數
(5)getActiveCount():當前執行緒池中正在執行任務的執行緒數量

好了,今天就到這兒吧,我是冰河,我們下期見~~

相關文章