高併發之——不得不說的線程池與ThreadPoolExecutor類淺析

1、拋磚引玉

既然Java中支持以多線程的方式來執行相應的任務,但爲何在JDK1.5中又提供了線程池技術呢?這個問題你們自行腦補,多動腦,確定沒壞處,哈哈哈。。。segmentfault

提及Java中的線程池技術,在不少框架和異步處理中間件中都有涉及,並且性能經受起了長久的考驗。能夠這樣說,Java的線程池技術是Java最核心的技術之一,在Java的高併發領域中,Java的線程池技術是一個永遠繞不開的話題。既然Java的線程池技術這麼重要(怎麼能說是這麼重要呢?那是至關的重要,那傢伙老重要了,哈哈哈),那麼,本文咱們就來簡單的說下線程池與ThreadPoolExecutor類。至於線程池中的各個技術細節和ThreadPoolExecutor的底層原理和源碼解析,咱們會在【高併發專題】專欄中進行深度解析。緩存

引言:本文是高併發中線程池的開篇之做,就暫時先不深刻講解,只是讓你們從總體上認識下線程池中最核心的類之一——ThreadPoolExecutor,關於ThreadPoolExecutor的底層原理和源碼實現,以及線程池中的其餘技術細節的底層原理和源碼實現,咱們會在【高併發專題】接下來的文章中,進行死磕。微信

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

(1)每次new Thread新建對象,性能差。
(2)線程缺少統一管理,可能無限制的新建線程,相互競爭,有可能佔用過多系統資源致使死機或OOM。
(3)缺乏更多的功能,如更多執行、按期執行、線程中斷。
(4)其餘弊端,你們自行腦補,多動腦,沒壞處,哈哈。多線程

3、線程池的好處

(1)重用存在的線程,減小對象建立、消亡的開銷,性能佳。
(2)能夠有效控制最大併發線程數,提升系統資源利用率,同時能夠避免過多資源競爭,避免阻塞。
(3)提供定時執行、按期執行、單線程、併發數控制等功能。
(4)提供支持線程池監控的方法,可對線程池的資源進行實時監控。
(5)其餘好處,你們自行腦補,多動腦,沒壞處,哈哈。併發

4、線程池

1.線程池類結構關係

線程池中的一些接口和類的結構關係以下圖所示。框架

file

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

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)性能

5、線程池最核心的類之一——ThreadPoolExecutor

1.構造方法

ThreadPoolExecutor參數最多的構造方法以下:spa

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

關注【冰河技術】微信公衆號,天天推送深度技術乾貨

相關文章
相關標籤/搜索