天天看點

高并發中,那些不得不說的線程池與ThreadPoolExecutor類

摘要:從整體上認識下線程池中最核心的類之一——ThreadPoolExecutor,關于ThreadPoolExecutor的底層原理和源碼實作,以及線程池中的其他技術細節的底層原理和源碼實作。

本文分享自華為雲社群《高并發之——不得不說的線程池與ThreadPoolExecutor類淺析》,作者: 冰 河 。

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

說起中的線程池技術,在很多架構和異步進行中間件中都有涉及,而且性能經受起了長久的考驗。可以這樣說,Java的線程池技術是Java最核心的技術之一,在Java的高并發領域中,Java的線程池技術是一個永遠繞不開的話題。既然Java的線程池技術這麼重要(怎麼能說是這麼重要呢?那是相當的重要,那家夥老重要了,哈哈哈),那麼,本文我們就來簡單的說下線程池與ThreadPoolExecutor類。

一、Thread直接建立線程的弊端

(1)每次new Thread建立對象,性能差。

(2)線程缺乏統一管理,可能無限制的建立線程,互相競争,有可能占用過多系統資源導緻當機或OOM。

(3)缺少更多的功能,如更多執行、定期執行、線程中斷。

(4)其他弊端,大家自行腦補,多動腦,沒壞處,哈哈。

二、線程池的好處

(1)重用存在的線程,減少對象建立、消亡的開銷,性能佳。

(2)可以有效控制最大并發線程數,提高系統資源使用率,同時可以避免過多資源競争,避免阻塞。

(3)提供定時執行、定期執行、單線程、并發數控制等功能。

(4)提供支援線程池監控的方法,可對線程池的資源進行實時監控。

(5)其他好處,大家自行腦補,多動腦,沒壞處,哈哈。

三、線程池

1.線程池類結構關系

線程池中的一些接口和類的結構關系如下圖所示。

高并發中,那些不得不說的線程池與ThreadPoolExecutor類

後文會死磕這些接口和類的底層原理和源碼。

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():目前線程池中正在執行任務的線程數量

點選關注,第一時間了解華為雲新鮮技術~