線程池的源碼從剛開始學Java就在看,剛開始看得很痛苦,縱然師父給我手把手講過一遍,我依然是半懂半不懂。如今距離剛開始學Java過去一年了,可能一方面是本身對Java語言愈來愈熟,另外一方面是用到了線程池的相關知識,再來看源碼,已經沒那麼吃力了java
一點心得:編程
JDK的源碼是必定要看的,只要你學Java。這裏的看不僅是跟着我或者其餘人的博文看過一遍就算看了,是本身要硬生生去親自啃這塊骨頭。爲何呢?由於「橫當作嶺側成峯」,同一本書,同一段代碼在不一樣的人的眼中,內容是不一樣的。寫博客的人會着重講本身認爲重要的,忽略掉一些不重要的部分,而可能對於初學者的你或者我,這些「不重要」的部分,咱們是不懂的。可是也不意味着看別人博文沒意思,有些博文確實e......可是用心寫的博文,通常加入了做者的思考和經驗,這些都是無形的財富。併發
說這些不是要你一開始就去啃這塊難啃的骨頭,最近在看《軟技能》,對裏面的學習法感同身受。學習新知識並非一開始就要看很深刻的東西,而是先了解最簡單的,基礎,去用,不會再看,再用。好比學習線程池,咱們先了解什麼是線程池,有什麼好處,有什麼內容,怎麼用。再結合我或者他人的博客去看看源碼,再本身去看看源碼,當本身能輸出(博文,或者講給其餘人聽)的時候,就算懂了。異步
瞭解ThreadPool,必定要看Doug Lea大神的註釋,私覺得不少博客寫的還不如大神的註釋,本節內容基本是註釋原文翻譯,括號內是本人加入的一些補充。下一節有讀源碼的我的經驗和源碼的解讀。函數
線程池解決了2個問題:源碼分析
每一個線程池都維護了一些基礎的統計信息,例如完成的任務數。性能
線程池能夠經過core size和max size動態調節池的大小(線程數的多少)。學習
當一個新任務經過 execute(Runnable)
提交時,若是工做線程數 < core size,那麼線程池會新建一個線程來處理這個新任務,即便這些工做線程處於空閒的狀態(也就是沒有處理任務)。線程
若是工做線程數 > core size,可是 < max size,那麼這個任務會被塞入隊列,除非隊列滿了。翻譯
若是設置core size=max size,那麼實際上,你建立了一個固定大小的線程池
若是設置max size 爲無限大,好比Integer.MAX_VALUE
,那麼這個線程池能夠同時處理任意多個任務。
一般狀況下,core size 和max size在初始化的時候就設置好了。固然,你能夠隨時經過setCorePoolSize
和setMaximumPoolSize
方法更改。
默認狀況下,當新任務到達時,工做線程纔會被建立。可是你能夠經過prestartCoreThread
或者prestartAllCoreThreads
方法預先讓core size個線程提早啓動。當你初始化的時候,若是傳入的隊列是非空的(也就是已經有任務「火燒眉毛」地待執行),這個時候,你須要提早準備好運行的線程(具體查看下一節的源碼分析,就知道緣由了)。
新線程由ThreadFactory
工廠建立,若是你沒有提供,線程池將使用Executors#defaultThreadFactory
,也就是默認的工廠類來構造。經過默認工廠建立的線程都在一個線程組(thread group)裏,他們擁有一樣的「NORM_PRIORITY」優先級和「非守護線程」的配置。若是你提供了不一樣的工廠,你能夠修改線程的名字,線程組,優先級,守護狀態等等。
當工廠新建線程失敗,池會繼續運行,可是可能無法處理任何任務。線程應該擁有名爲「modifyThread」的運行時權限(RuntimePermission).若是工做線程,或者其餘線程使用這個池,可是沒有擁有這個權限,服務可能會退化:配置雖然修改了,可是沒有及時起效,而且一個關閉(SHUTDOWN)的池可能處於終止但可能未完成的狀態。
若是如今線程池的線程數量 > core size ,其中某個線程的空閒時間(一直沒有拿到任務) > keep-alive time,那麼這個線程會終止。這個方式能夠在線程池沒有太多任務的時候,用來下降線程資源的消耗。 keep-alive time能夠經過setKeepAliveTime(TimeUnit)
方法動態更改。若是傳入Long.Max_VALUE
,那麼空閒線程將永遠不會被終止。默認狀況下,只有線程數超過core size, 超時策略纔會生效。但allowCoreThreadTimeOut(boolean)
方法可讓超時策略在線程數小於 core size時候也生效,只要keep-alive time非0。
任何的阻塞隊列能夠傳遞和接收任務,可是具體策略和線程池的大小有關:
線程池的狀態貫穿了線程池的整個生命週期,有如下5個生命週期:
RUNNING: 接收新任務,處理隊列的任務。SHUTDOWN:不接收新任務,繼續處理隊列的任務。
STOP:不接收新任務,也不處理隊列裏的任務,並嘗試中止正在運行的任務。
TIDYING:全部的任務都終止了,線程數爲0以後,線程池狀態會過分到TIDYING,而後執行terminated()鉤子方法。
TERMINATED:在terminated()方法執行完以後,線程池狀態就會變成TERTMINATED。
每一個狀態對應的數值很重要,用於後續的比較,每一個狀態的數值遞增,但狀態的變化並不須要連貫。有如下幾種變化形式:
RUNNING -> SHUTDOWN:當調用shutdown()
或者finalize()
方法時(RUNNING or SHUTDOWN) -> STOP:調用
shutdownNow()
方法時SHUTDOWN -> TIDYING:當隊列和池都空了的時候
STOP -> TIDYING:當池空了
TIDYING -> TERMINATED:當
terminated()
方法結束的時候。
線程池有如下三個入隊策略:
a. 直接傳遞:
一個優秀的默認隊列是SynchronousQueue.它會在任務入隊後,馬上將任務轉給線程處理,而不保留任務。若是沒有可用的線程(無法新建更多的線程)來處理新任務,那麼會入隊失敗。這個策略能夠避免任務被鎖住(線程的飢餓死鎖,查看頁尾補充說明)
b. 無界隊列:
在線程池中使用無界隊列(例如沒有預設容量的LinkedBlockingQueue),意味着池中若是有core size個線程正在運行,那麼新來的任務會所有塞入這個隊列。所以,該線程池最多存在core size個工做線程(max size將會失效)。當任務彼此不相關時,這是一個很好的作法。例如,無界隊列能夠容納突如其來的順勢爆發的請求,即便請求到來的速度超出服務的處理速度。
c. 有界隊列:
有界隊列(例如 ArrayBlockingQueue)能夠經過設定max size來保護資源,但同時也更難協調和控制。隊列的長度和池的大小須要相互協調:
長隊列和小(線程池)池的組合減小了CPU的使用,OS 資源和上下文切換帶來的損耗,可是可能會人爲地下降吞吐量。若是任務常常阻塞(例如I/O密集型任務),系統能夠爲更多的線程安排時間,可能比你設定的線程數還要多(沒有充分利用CPU)。
短隊列一般須要和大(線程)池搭配使用,它們能充分利用CPU,可是也可能會帶來不可預計的調度開銷,於是下降吞吐量。
當線程池SHUTDOWN以後,或者在設定了固定的池的最大線程數和隊列長度,並都處於飽和的狀態下,經過execute(Runnable)
方法提交的任務會被拒絕。在上述兩種狀況下,execute
方法會調用RejectedExecutionHandler#rejectedExecution(Runnable,ThreadPoolExecutor)
方法,RejectedExecutionHandler
是一個接口,每一個線程池的RejectedExecutionHandler
變量不同,該接口有四種具體實現:
ThreadPoolExecutor.AbortPolicy
(默認):拒絕新任務,並拋出RejectedExecutionException
異常。ThreadPoolExecutor.CallerRunsPolicy
: 調用execute方法的線程自己來執行這個任務。這種作法提供了一個簡易的反饋控制機制下降新任務的提交頻率。ThreadPoolExecutor.DiscardPolicy
:直接丟棄。ThreadPoolExecutor.DiscardOldestPolicy
:線程池正常運行的狀況下,放棄最舊的未處理請求,而後重試 execute
;若是執行程序已關閉,則會丟棄該任務。固然,你也能夠自定義其餘的拒絕策略。這時你須要格外當心,尤爲在你的策略應用於特定的池的大小,或者排隊策略上時。
ThreadPoolExecutor
類提供 beforeExecute(Thread, Runnable)
和
afterExecute(Runnable, Throwable)
} 兩個可被覆蓋的鉤子函數。它們在任務的開始和結束的時候被調用。可被用於配置運行環境,例如更改ThreadLocals
,收集統計信息,或者加日誌。此外,terminated()
方法也能夠被覆蓋,在線程池徹底終止的時候,你能夠經過這個方法作一些特殊的處理。
若是鉤子方法拋出異常,內部的工做線程可能會逐個失敗直至線程池終止。
getQueue()
能夠獲取隊列來監控和調試,強烈不建議你們使用這個方法來達到其餘目的。當大量的入隊任務被取消時,remove(Runnable)
和 purge
方法能夠幫助來回收空間。
當一個線程池再也不被其餘程序引用,而且池中沒有線程的時候,就會自動shut down。若是你但願一個再也不被引用的線程池能夠被自動回收(都說是自動,固然不是手動使用shutdown方法),那麼你必須確保空閒線程會自動中止。你能夠經過設置keep-alive time,core size設爲0,而且要記住調用allowCoreThreadTimeOut
方法使keep-alive time在全部線程上都能生效。
這是一個使用線程池的例子,咱們新增了一個簡單的中止/恢復 功能:
class PausableThreadPoolExecutor extends ThreadPoolExecutor { private boolean isPaused; private ReentrantLock pauseLock = new ReentrantLock(); private Condition unpaused = pauseLock.newCondition(); public PausableThreadPoolExecutor(...) { super(...); } protected void beforeExecute(Thread t, Runnable r) { super.beforeExecute(t, r); pauseLock.lock(); try { while (isPaused) unpaused.await(); } catch (InterruptedException ie) { t.interrupt(); } finally { pauseLock.unlock(); } } public void pause() { pauseLock.lock(); try { isPaused = true; } finally { pauseLock.unlock(); } } public void resume() { pauseLock.lock(); try { isPaused = false; unpaused.signalAll(); } finally { pauseLock.unlock(); } } }
1.線程飢餓死鎖(《Java併發編程實戰》):在線程池,若是任務依賴於任務,那麼可能產生死鎖。在單線程的Executor中,若是一個任務將另外一個任務提交到同一個Executor,而且等待這個被提交的結果,那麼一般會發生死鎖。若是正在執行的線程都因爲等待其餘仍處於工做隊列的任務而阻塞,這種現象稱爲飢餓死鎖(Thread Starvation Deadlock)。只要線程池中的的任務,須要無限期等待一些必須由池中其餘任務才能提供的資源,或者條件,例如某個任務等待另外一個任務的返回值或者執行結果,那麼除非這個池夠大,不然將發生線程飢餓死鎖。