JDK源碼學習1-ThreadPoolExecutor學習,先看註釋

寫在開篇

線程池的源碼從剛開始學Java就在看,剛開始看得很痛苦,縱然師父給我手把手講過一遍,我依然是半懂半不懂。如今距離剛開始學Java過去一年了,可能一方面是本身對Java語言愈來愈熟,另外一方面是用到了線程池的相關知識,再來看源碼,已經沒那麼吃力了java

一點心得:編程

JDK的源碼是必定要看的,只要你學Java。這裏的看不僅是跟着我或者其餘人的博文看過一遍就算看了,是本身要硬生生去親自啃這塊骨頭。爲何呢?由於「橫當作嶺側成峯」,同一本書,同一段代碼在不一樣的人的眼中,內容是不一樣的。寫博客的人會着重講本身認爲重要的,忽略掉一些不重要的部分,而可能對於初學者的你或者我,這些「不重要」的部分,咱們是不懂的。可是也不意味着看別人博文沒意思,有些博文確實e......可是用心寫的博文,通常加入了做者的思考和經驗,這些都是無形的財富。併發

說這些不是要你一開始就去啃這塊難啃的骨頭,最近在看《軟技能》,對裏面的學習法感同身受。學習新知識並非一開始就要看很深刻的東西,而是先了解最簡單的,基礎,去用,不會再看,再用。好比學習線程池,咱們先了解什麼是線程池,有什麼好處,有什麼內容,怎麼用。再結合我或者他人的博客去看看源碼,再本身去看看源碼,當本身能輸出(博文,或者講給其餘人聽)的時候,就算懂了。異步

瞭解ThreadPool,必定要看Doug Lea大神的註釋,私覺得不少博客寫的還不如大神的註釋,本節內容基本是註釋原文翻譯,括號內是本人加入的一些補充。下一節有讀源碼的我的經驗和源碼的解讀。函數

1. 線程池功能

線程池解決了2個問題:源碼分析

  1. 在執行大量異步任務時,經過減小每一個任務的調用開銷,提升了性能(不用重複建立銷燬線程等)。
  2. 它提供了能夠管理並限制資源的方法,這些資源包括線程(限制線程池的大小,隊列大小等等)。

    每一個線程池都維護了一些基礎的統計信息,例如完成的任務數。性能

2. 可調節的參數和鉤子

2.1 線程池的core size和max size:

線程池能夠經過core sizemax size動態調節池的大小(線程數的多少)。學習

當一個新任務經過 execute(Runnable)提交時,若是工做線程數 < core size,那麼線程池會新建一個線程來處理這個新任務,即便這些工做線程處於空閒的狀態(也就是沒有處理任務)。線程

若是工做線程數 > core size,可是 < max size,那麼這個任務會被塞入隊列,除非隊列滿了。翻譯

若是設置core size=max size,那麼實際上,你建立了一個固定大小的線程池

若是設置max size 爲無限大,好比Integer.MAX_VALUE,那麼這個線程池能夠同時處理任意多個任務。

一般狀況下,core sizemax size在初始化的時候就設置好了。固然,你能夠隨時經過setCorePoolSizesetMaximumPoolSize方法更改。

2.2 根據需求進行初始化

默認狀況下,當新任務到達時,工做線程纔會被建立。可是你能夠經過prestartCoreThread或者prestartAllCoreThreads方法預先讓core size個線程提早啓動。當你初始化的時候,若是傳入的隊列是非空的(也就是已經有任務「火燒眉毛」地待執行),這個時候,你須要提早準備好運行的線程(具體查看下一節的源碼分析,就知道緣由了)。

2.3 新建線程

新線程由ThreadFactory工廠建立,若是你沒有提供,線程池將使用Executors#defaultThreadFactory,也就是默認的工廠類來構造。經過默認工廠建立的線程都在一個線程組(thread group)裏,他們擁有一樣的「NORM_PRIORITY」優先級和「非守護線程」的配置。若是你提供了不一樣的工廠,你能夠修改線程的名字,線程組,優先級,守護狀態等等。

當工廠新建線程失敗,池會繼續運行,可是可能無法處理任何任務。線程應該擁有名爲「modifyThread」的運行時權限(RuntimePermission).若是工做線程,或者其餘線程使用這個池,可是沒有擁有這個權限,服務可能會退化:配置雖然修改了,可是沒有及時起效,而且一個關閉(SHUTDOWN)的池可能處於終止但可能未完成的狀態。

2.4 Keep-alive 時間

​ 若是如今線程池的線程數量 > core size ,其中某個線程的空閒時間(一直沒有拿到任務) > keep-alive time,那麼這個線程會終止。這個方式能夠在線程池沒有太多任務的時候,用來下降線程資源的消耗。 keep-alive time能夠經過setKeepAliveTime(TimeUnit)方法動態更改。若是傳入Long.Max_VALUE,那麼空閒線程將永遠不會被終止。默認狀況下,只有線程數超過core size, 超時策略纔會生效。但allowCoreThreadTimeOut(boolean)方法可讓超時策略在線程數小於 core size時候也生效,只要keep-alive time非0。

2.5 入隊

任何的阻塞隊列能夠傳遞和接收任務,可是具體策略和線程池的大小有關:

  1. 若是線程數 < core size,線程池會新建線程來處理新任務,而不會塞入隊列。
  2. 若是線程數 >= core size,新任務會入隊。
  3. 若是新任務入隊失敗(例如隊列滿了),而且線程數量 < max size,那麼會新建線程處理任務。反之若是線程數= max size,那麼任務會被拒絕。

線程池的狀態貫穿了線程池的整個生命週期,有如下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,可是也可能會帶來不可預計的調度開銷,於是下降吞吐量。

2.6 拒絕任務

當線程池SHUTDOWN以後,或者在設定了固定的池的最大線程數和隊列長度,並都處於飽和的狀態下,經過execute(Runnable)方法提交的任務會被拒絕。在上述兩種狀況下,execute方法會調用RejectedExecutionHandler#rejectedExecution(Runnable,ThreadPoolExecutor)方法,RejectedExecutionHandler是一個接口,每一個線程池的RejectedExecutionHandler變量不同,該接口有四種具體實現:

  1. ThreadPoolExecutor.AbortPolicy(默認):拒絕新任務,並拋出RejectedExecutionException異常。
  2. ThreadPoolExecutor.CallerRunsPolicy : 調用execute方法的線程自己來執行這個任務。這種作法提供了一個簡易的反饋控制機制下降新任務的提交頻率。
  3. ThreadPoolExecutor.DiscardPolicy:直接丟棄。
  4. ThreadPoolExecutor.DiscardOldestPolicy:線程池正常運行的狀況下,放棄最舊的未處理請求,而後重試 execute;若是執行程序已關閉,則會丟棄該任務。

固然,你也能夠自定義其餘的拒絕策略。這時你須要格外當心,尤爲在你的策略應用於特定的池的大小,或者排隊策略上時。

2.7 鉤子方法

ThreadPoolExecutor類提供 beforeExecute(Thread, Runnable)

afterExecute(Runnable, Throwable)} 兩個可被覆蓋的鉤子函數。它們在任務的開始和結束的時候被調用。可被用於配置運行環境,例如更改ThreadLocals,收集統計信息,或者加日誌。此外,terminated()方法也能夠被覆蓋,在線程池徹底終止的時候,你能夠經過這個方法作一些特殊的處理。

若是鉤子方法拋出異常,內部的工做線程可能會逐個失敗直至線程池終止

2.8 隊列維護

getQueue()能夠獲取隊列來監控和調試,強烈不建議你們使用這個方法來達到其餘目的。當大量的入隊任務被取消時,remove(Runnable)purge方法能夠幫助來回收空間。

2.9 回收線程池

當一個線程池再也不被其餘程序引用,而且池中沒有線程的時候,就會自動shut down。若是你但願一個再也不被引用的線程池能夠被自動回收(都說是自動,固然不是手動使用shutdown方法),那麼你必須確保空閒線程會自動中止。你能夠經過設置keep-alive time,core size設爲0,而且要記住調用allowCoreThreadTimeOut方法使keep-alive time在全部線程上都能生效。

3 用例

這是一個使用線程池的例子,咱們新增了一個簡單的中止/恢復 功能:

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();
        }
    }
}

4. 補充說明

1.線程飢餓死鎖(《Java併發編程實戰》):

在線程池,若是任務依賴於任務,那麼可能產生死鎖。在單線程的Executor中,若是一個任務將另外一個任務提交到同一個Executor,而且等待這個被提交的結果,那麼一般會發生死鎖。若是正在執行的線程都因爲等待其餘仍處於工做隊列的任務而阻塞,這種現象稱爲飢餓死鎖(Thread Starvation Deadlock)。只要線程池中的的任務,須要無限期等待一些必須由池中其餘任務才能提供的資源,或者條件,例如某個任務等待另外一個任務的返回值或者執行結果,那麼除非這個池夠大,不然將發生線程飢餓死鎖。

相關文章
相關標籤/搜索