更好的使用JAVA線程池


這篇文章結合Doug Lea大神在JDK1.5提供的JCU包,分別從線程池大小參數的設置、工做線程的建立、空閒線程的回收、阻塞隊列的使用、任務拒絕策略、線程池Hook等方面來了解線程池的使用,其中涉及到一些細節包括不一樣參數、不一樣隊列、不一樣拒絕策略的選擇、產生的影響和行爲、爲更好的使用線程池奠基知識基礎,其中值得注意的部分我用粗體標識。服務器

Doug Lea多線程

ExecutorService基於池化的線程來執行用戶提交的任務,一般能夠簡單的經過Executors提供的工廠方法來建立ThreadPoolExecutor實例。
異步

線程池解決的兩個問題:1)線程池經過減小每次作任務的時候產生的性能消耗來優化執行大量的異步任務的時候的系統性能。2)線程池還提供了限制和管理批量任務被執行的時候消耗的資源、線程的方法。另外ThreadPoolExecutor還提供了簡單的統計功能,好比當前有多少任務被執行完了。性能


快速開始優化

爲了使得線程池適合大量不一樣的應用上下文環境,ThreadPoolExecutor提供了不少能夠配置的參數和可被用來擴展的鉤子。然而,用戶還能夠經過使用Executors提供的一些工廠方法來快速建立ThreadPoolExecutor實例。好比:spa

  1. 使用Executors#newCachedThreadPool能夠快速建立一個擁有自動回收線程功能且沒有限制的線程池。操作系統

  2. 使用Executors#newFixedThreadPool能夠用來建立一個固定線程大小的線程池。線程

  3. 使用Executors#newSingleThreadExecutor能夠用來建立一個單線程的執行器。debug

若是上面的方法建立的實例不能知足咱們的需求,咱們能夠本身經過參數來配置,實例化一個實例。rest


關於線程數大小參數設置須要知道的

ThreadPoolExecutor會根據corePoolSize和maximumPoolSize來動態調整線程池的大小:poolSize。

當任務經過executor提交給線程池的時候,咱們須要知道下面幾個點:

  1. 若是這個時候當前池子中的工做線程數小於corePoolSize,則新建立一個新的工做線程來執行這個任務,無論工做線程集合中有沒有線程是處於空閒狀態。

  2. 若是池子中有比corePoolSize大的可是比maximumPoolSize小的工做線程,任務會首先被嘗試着放入隊列,這裏有兩種狀況須要單獨說一下:

    a、若是任務唄成功的放入隊列,則看看是否須要開啓新的線程來執行任務,只有噹噹前工做線程數爲0的時候纔會建立新的線程,由於以前的線程有可能由於都處於空閒狀態或由於工做結束而被移除。

    b、若是放入隊列失敗,則纔會去建立新的工做線程。

  3. 若是corePoolSize和maximumPoolSize相同,則線程池的大小是固定的。

  4. 經過將maximumPoolSize設置爲無限大,咱們能夠獲得一個無上限的線程池。

  5. 除了經過構造參數設置這幾個線程池參數以外咱們還能夠在運行時設置。


核心線程WarmUp

默認狀況下,核心工做線程值在初始的時候被建立,當新任務來到的時候被啓動,可是咱們能夠經過重寫prestartCoreThread或prestartCoreThreads方法來改變這種行爲。一般場景咱們能夠在應用啓動的時候來WarmUp核心線程,從而達到任務過來可以立馬執行的結果,使得初始任務處理的時間獲得必定優化。


定製工做線程的建立

新的線程是經過ThreadFactory來建立的,若是沒有指定,默認的Executors#defaultThreadFactory將被使用,這個時候建立的線程將都屬於同一個線程組,擁有一樣的優先級和daemon狀態。擴展配置ThreadFactory,咱們能夠配置線程的名字、線程組合daemon狀態。若是調用ThreadFactory#createThread的時候失敗,將返回null,executor將不會執行任何任務。


空閒線程回收

若是當前池子中的工做線程數大於corePoolSize,若是超過這個數字的線程處於空閒的時間大於keepAliveTime,則這些線程將會被終止,這是一種減小沒必要要資源消耗的策略。這個參數能夠在運行時被改變,咱們一樣能夠將這種策略應用給核心線程,咱們能夠經過調用allowCoreThreadTimeout來實現。


選擇合適的阻塞隊列

全部的阻塞隊列均可以被用來存聽任務,可是使用不一樣的隊列針對corePoolSize會表現不一樣的行爲:

當池中工做線程數小於corePoolSize的時候,每次來任務的時候都會建立一個新的工做線程。

當池中工做線程數大於等於corePoolSize的時候,每次任務來的時候都會首先嚐試將線程放入隊列,而不是直接去建立線程。

若是放入隊列失敗,且當先池中線程數小於maximumPoolSize的時候,則會建立一個工做線程。

下面主要是不一樣隊列策略表現:

直接遞交:一種比較好的默認選擇是使用SynchronousQueue,這種策略會將提交的任務直接傳送給工做線程,而不持有。若是當前沒有工做線程來處理,即任務放入隊列失敗,則根據線程池的實現,會引起新的工做線程建立,所以新提交的任務會被處理。這種策略在當提交的一批任務之間有依賴關係的時候避免了鎖競爭消耗。值得一提的是,這種策略最好是配合unbounded線程數來使用,從而避免任務被拒絕。同時咱們必需要考慮到一種場景,當任務到來的速度大於任務處理的速度,將會引發無限制的線程數不斷的增長。

無界隊列:使用無界隊列如LinkedBlockingQueue沒有指定最大容量的時候,將會引發當核心線程都在忙的時候,新的任務被放在隊列上,所以,永遠不會有大於corePoolSize的線程被建立,所以maximumPoolSize參數將失效。這種策略比較適合全部的任務都不相互依賴,獨立執行。舉個例子,如網頁服務器中,每一個線程獨立處理請求。可是當任務處理速度小於任務進入速度的時候會引發隊列的無限膨脹。

有界隊列:有界隊列如ArrayBlockingQueue幫助限制資源的消耗,可是不容易控制。隊列長度和maximumPoolSize這兩個值會相互影響,使用大的隊列和小maximumPoolSize會減小CPU的使用、操做系統資源、上下文切換的消耗,可是會下降吞吐量,若是任務被頻繁的阻塞如IO線程,系統其實能夠調度更多的線程。使用小的隊列一般須要大maximumPoolSize,從而使得CPU更忙一些,可是又會增長下降吞吐量的線程調度的消耗。總結一下是IO密集型能夠考慮多些線程來平衡CPU的使用,CPU密集型能夠考慮少些線程減小線程調度的消耗。


選擇適合的拒絕策略

當新的任務到來的而線程池被關閉的時候,或線程數和隊列已經達到上限的時候,咱們須要去作一個決定,怎麼拒絕這些任務。下面介紹一下經常使用的策略:

ThreadPoolExecutor#AbortPolicy:這個策略直接拋出RejectedExecutionException異常。

ThreadPoolExecutor#CallerRunsPolicy:這個策略將會使用Caller線程來執行這個任務,這是一種feedback策略,能夠下降任務提交的速度。

ThreadPoolExecutor#DiscardPolicy:這個策略將會直接丟棄任務。

ThreadPoolExecutor#DiscardOldestPolicy:這個策略將會把任務隊列頭部的任務丟棄,而後從新嘗試執行,若是仍是失敗則繼續實施策略。

除了上面的幾種策略,咱們也能夠經過實現RejectedExecutionHandler來實現本身的策略。


利用Hook嵌入你的行爲

ThreadPoolExecutor提供了protected類型能夠被覆蓋的鉤子方法,容許用戶在任務執行以前會執行以後作一些事情。咱們能夠經過它來實現好比初始化ThreadLocal、收集統計信息、如記錄日誌等操做。這類Hook如beforeExecute和afterExecute。另外還有一個Hook能夠用來在任務被執行完的時候讓用戶插入邏輯,如rerminated。

若是hook方法執行失敗,則內部的工做線程的執行將會失敗或被中斷。


可訪問的隊列

getQueue方法能夠用來訪問queue隊列以進行一些統計或者debug工做,咱們不建議用做其餘用途。同時remove方法和purge方法能夠用來將任務從隊列中移除。


關閉線程池

當線程池不在被引用而且工做線程數爲0的時候,線程池將被終止。咱們也能夠調用shutdown來手動終止線程池。若是咱們忘記調用shutdown,爲了讓線程資源被釋放,咱們還可使用keepAliveTime和allowCoreThreadTimeOut來達到目的。


寫在最後

JAVA自己提供的API已經可讓咱們快速的進行基於線程池的多線程開發,可是咱們必需要爲咱們寫的代碼負責,每個參數的設置和策略的選擇跟不一樣應用場景有絕對的關係。然而對於不一樣參數和不一樣策略的選擇並非一件容易的事情,咱們必需要先回答一些基礎問題:每建立一個線程,操做系統爲咱們作了哪些事情,這個線程的操做系統資源消耗主要在哪部分?假如個人應用場景是IO密集型的,那麼我須要更多的線程仍是更少的線程?假如咱們的CPU操做和IO操做大概各佔一半的話咱們又須要如何選擇?等等一些列問題。我認爲、多線程開發是一件很容易的事情也是一件很不容易的事情。:)


參考文檔《JDK1.5》 by Dong Lea

相關文章
相關標籤/搜索