深入源碼,深度解析Java 執行緒池的實現原理

  • 2021 年 5 月 26 日
  • 筆記

java 系統的運行歸根到底是程式的運行,程式的運行歸根到底是程式碼的執行,程式碼的執行歸根到底是虛擬機的執行,虛擬機的執行其實就是作業系統的執行緒在執行,並且會佔用一定的系統資源,如CPU、記憶體、磁碟、網路等等。所以,如何高效的使用這些資源就是程式設計師在平時寫程式碼時候的一個努力的方向。本文要說的執行緒池就是一種對 CPU 利用的優化手段。

執行緒池,百度百科是這麼解釋的:

執行緒池是一種多執行緒處理形式,處理過程中將任務添加到隊列,然後在創建執行緒後自動啟動這些任務。執行緒池執行緒都是後台執行緒。每個執行緒都使用默認的堆棧大小,以默認的優先順序運行,並處於多執行緒單元中。如果某個執行緒在託管程式碼中空閑(如正在等待某個事件),則執行緒池將插入另一個輔助執行緒來使所有處理器保持繁忙。如果所有執行緒池執行緒都始終保持繁忙,但隊列中包含掛起的工作,則執行緒池將在一段時間後創建另一個輔助執行緒但執行緒的數目永遠不會超過最大值。超過最大值的執行緒可以排隊,但他們要等到其他執行緒完成後才啟動。

執行緒池,其實就是維護了很多執行緒的池子,類似這樣的技術還有很多的,例如:HttpClient 連接池、資料庫連接池、記憶體池等等。

執行緒池的優點

在 Java 並發編程框架中的執行緒池是運用場景最多的技術,幾乎所有需要非同步或並發執行任務的程式都可以使用執行緒池。在開發過程中,合理地使用執行緒池能夠帶來至少以下4個好處。

第一:降低資源消耗。通過重複利用已創建的執行緒降低執行緒創建和銷毀造成的消耗;

第二:提高響應速度。當任務到達時,任務可以不需要等到執行緒創建就能立即執行;

第三:提高執行緒的可管理性。執行緒是稀缺資源,如果無限制地創建,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一分配、調優和監控。

第四:提供更強大的功能,比如延時定時執行緒池;

執行緒池的實現原理

當向執行緒池提交一個任務之後,執行緒池是如何處理這個任務的呢?下面就先來看一下它的主要處理流程。先來看下面的這張圖,然後我們一步一步的來解釋。

image-20210322132334799

當使用者將一個任務提交到執行緒池以後,執行緒池是這麼執行的:

①首先判斷核心的執行緒數是否已滿,如果沒有滿,那麼就去創建一個執行緒去執行該任務;否則請看下一步

②如果執行緒池的核心執行緒數已滿,那麼就繼續判斷任務隊列是否已滿,如果沒滿,那麼就將任務放到任務隊列中;否則請看下一步

③如果任務隊列已滿,那麼就判斷執行緒池是否已滿,如果沒滿,那麼就創建執行緒去執行該任務;否則請看下一步;

④如果執行緒池已滿,那麼就根據拒絕策略來做出相應的處理;

上面的四步其實就已經將執行緒池的執行原理描述結束了。如果不明白沒有關係,先一步一步往下看,上面涉及到的執行緒池的專有名詞都會詳細的介紹到。

我們在平時的開發中,執行緒池的使用基本都是基於ThreadPoolExexutor類,他的繼承體系是這樣子的:

image-20210322133058425

那既然說在使用中都是基於ThreadPoolExecutor的那麼我們就重點分析這個類。

至於他構造體系中的其他的類或者是介面中的屬性,這裡就不去截圖了,完全沒有必要。小夥伴如果實在想看就自己去打開程式碼看一下就行了。

ThreadPoolExecutor

在《阿里巴巴 java 開發手冊》中指出了執行緒資源必須通過執行緒池提供,不允許在應用中自行顯示的創建執行緒,這樣一方面是執行緒的創建更加規範,可以合理控制開闢執行緒的數量;另一方面執行緒的細節管理交給執行緒池處理,優化了資源的開銷。

其原文描述如下:

ThreadPoolExecutor類中提供了四個構造方法,但是他的四個構造器中,實際上最終都會調用同一個構造器,只不過是在另外三個構造器中,如果有些參數不傳ThreadPoolExecutor會幫你使用默認的參數。所以,我們直接來看這個完整參數的構造器,來徹底剖析裡面的參數。

public  class  ThreadPoolExecutor  extends  AbstractExecutorService {
    ......

    public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime,TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler) {
            if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0){
                throw new IllegalArgumentException();
            }
            if (workQueue == null || threadFactory == null || handler == null){
                throw new NullPointerException();
            }
            this.acc = System.getSecurityManager() == null ? null : AccessController.getContext();
            this.corePoolSize = corePoolSize;
            this.maximumPoolSize = maximumPoolSize;
            this.workQueue = workQueue;
            this.keepAliveTime = unit.toNanos(keepAliveTime);
            this.threadFactory = threadFactory;
            this.handler = handler;
        }
}

主要參數就是下面這幾個:

  • corePoolSize:執行緒池中的核心執行緒數,包括空閑執行緒,也就是核心執行緒數的大小;
  • maximumPoolSize:執行緒池中允許的最多的執行緒數,也就是說執行緒池中的執行緒數是不可能超過該值的;
  • keepAliveTime:當執行緒池中的執行緒數大於 corePoolSize 的時候,在超過指定的時間之後就會將多出 corePoolSize 的的空閑的執行緒從執行緒池中刪除;
  • unit:keepAliveTime 參數的單位(常用的秒為單位);
  • workQueue:用於保存任務的隊列,此隊列僅保持由 executor 方法提交的任務 Runnable 任務;
  • threadFactory:執行緒池工廠,他主要是為了給執行緒起一個標識。也就是為執行緒起一個具有意義的名稱;
  • handler:拒絕策略

阻塞隊列

workQueue 有多種選擇,在 JDK 中一共提供了 7 中阻塞對列,分別為:

  1. ArrayBlockingQueue : 一個由數組結構組成的有界阻塞隊列。 此隊列按照先進先出(FIFO)的原則對元素進行排序。默認情況下不保證訪問者公平地訪問隊列 ,所謂公平訪問隊列是指阻塞的執行緒,可按照阻塞的先後順序訪問隊列。非公平性是對先等待的執行緒是不公平的,當隊列可用時,阻塞的執行緒都可以競爭訪問隊列的資格。

  2. LinkedBlockingQueue : 一個由鏈表結構組成的有界阻塞隊列。 此隊列的默認和最大長度為Integer.MAX_VALUE。 此隊列按照先進先出的原則對元素進行排序。

  3. PriorityBlockingQueue : 一個支援優先順序排序的無界阻塞隊列。 (雖然此隊列邏輯上是無界的,但是資源被耗盡時試圖執行 add 操作也將失敗,導致 OutOfMemoryError)

  4. DelayQueue: 一個使用優先順序隊列實現的無界阻塞隊列。 元素的一個無界阻塞隊列,只有在延遲期滿時才能從中提取元素

  5. SynchronousQueue: 一個不存儲元素的阻塞隊列。 一種阻塞隊列,其中每個插入操作必須等待另一個執行緒的對應移除操作 ,反之亦然。(SynchronousQueue 該隊列不保存元素)

  6. LinkedTransferQueue: 一個由鏈表結構組成的無界阻塞隊列。 相對於其他阻塞隊列LinkedTransferQueue多了tryTransfer和transfer方法。

  7. LinkedBlockingDeque: 一個由鏈表結構組成的雙向阻塞隊列。 是一個由鏈表結構組成的雙向阻塞隊列

在以上的7個隊列中,執行緒池中常用的是ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue

隊列中的常用的方法如下:

類型 方法 含義 特點
拋異常 add 添加一個元素 如果隊列滿,拋出異常 IllegalStateException
拋異常 remove 返回並刪除隊列的頭節點 如果隊列空,拋出異常 NoSuchElementException
拋異常 element 返回隊列頭節點 如果隊列空,拋出異常 NoSuchElementException
不拋異常,但是不阻塞 offer 添加一個元素 添加成功,返回 true,添加失敗,返回 false
不拋異常,但是不阻塞 poll 返回並刪除隊列的頭節點 如果隊列空,返回 null
不拋異常,但是不阻塞 peek 返回隊列頭節點 如果隊列空,返回 null
阻塞 put 添加一個元素 如果隊列滿,阻塞
阻塞 take 返回並刪除隊列的頭節點 如果隊列空,阻塞

關於阻塞隊列,介紹到這裡也就基本差不多了。

執行緒池工廠

執行緒池工廠,就像上面已經介紹的,目的是為了給執行緒起一個有意義的名字。用起來也非常的簡單,只需要實現ThreadFactory介面即可

public class CustomThreadFactory implements ThreadFactory {
    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r);
        thread.setName("我是你們自己定義的執行緒名稱");
        return thread;
    }
}

具體的使用就不去廢話了。

拒絕策略

執行緒池有四種默認的拒絕策略,分別為:

  1. AbortPolicy:這是執行緒池默認的拒絕策略,在任務不能再提交的時候,拋出異常,及時回饋程式運行狀態。如果是比較關鍵的業務,推薦使用此拒絕策略,這樣子在系統不能承載更大的並發量的時候,能夠及時的通過異常發現;

  2. DiscardPolicy:丟棄任務,但是不拋出異常。如果執行緒隊列已滿,則後續提交的任務都會被丟棄,且是靜默丟棄。這玩意不建議使用;

  3. DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新提交被拒絕的任務。這玩意不建議使用;

  4. CallerRunsPolicy:如果任務添加失敗,那麼主執行緒就會自己調用執行器中的 executor 方法來執行該任務。這玩意不建議使用;

也就是說關於執行緒池的拒絕策略,最好使用默認的。這樣能夠及時發現異常。如果上面的都不能滿足你的需求,你也可以自定義拒絕策略,只需要實現 RejectedExecutionHandler 介面即可

public class CustomRejection implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println("你自己想怎麼處理就怎麼處理");
    }
}

看到這裡,我們再來畫一張圖來總結和概括下執行緒池的執行示意圖:

image-20210322145600064

詳細的執行過程全部在圖中說明了。

提交任務到執行緒池

在 java 中,有兩個方法可以將任務提交到執行緒池,分別是submitexecute

execute 方法

execute()方法用於提交不需要返回值的任務,所以無法判斷任務是否被執行緒池執行成功。

void execute(Runnable command);

通過以下程式碼可知 execute() 方法輸入的任務是一個Runnable類的實例。

executorService.execute(()->{
            System.out.println("ThreadPoolDemo.execute");
        });

submit 方法

submit()方法用於提交需要返回值的任務。

Future<?> submit(Runnable task);

執行緒池會返回一個future類型的對象,通過這個 future 對象可以判斷任務是否執行成功,並且可以通過future的get()方法來獲取返回值,get() 方法會阻塞當前執行緒直到任務完成,而使用get(long timeout,TimeUnit unit)方法則會阻塞當前執行緒一段時間後立即返回,這時候有可能任務沒有執行完。

Future<?> submit = executorService.submit(() -> {
            System.out.println("ThreadPoolDemo.submit");
        });

關閉執行緒池

其實,如果優雅的關閉執行緒池是一個令人頭疼的問題,執行緒開啟是簡單的,但是想要停止卻不是那麼容易的。通常而言, 大部分程式設計師都是使用 jdk 提供的兩個方法來關閉執行緒池,他們分別是:shutdownshutdownNow

通過調用執行緒池的 shutdownshutdownNow 方法來關閉執行緒池。它們的原理是遍歷執行緒池中的工作執行緒,然後逐個調用執行緒的 interrupt 方法來中斷執行緒(PS:中斷,僅僅是給執行緒打上一個標記,並不是代表這個執行緒停止了,如果執行緒不響應中斷,那麼這個標記將毫無作用),所以無法響應中斷的任務可能永遠無法終止。

但是它們存在一定的區別,shutdownNow首先將執行緒池的狀態設置成 STOP,然後嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任務的列表,而 shutdown 只是將執行緒池的狀態設置成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的執行緒。

只要調用了這兩個關閉方法中的任意一個,isShutdown 方法就會返回 true。當所有的任務都已關閉後,才表示執行緒池關閉成功,這時調用isTerminaed方法會返回 true。至於應該調用哪一種方法來關閉執行緒池,應該由提交到執行緒池的任務特性決定,通常調用 shutdown方法來關閉執行緒池,如果任務不一定要執行完,則可以調用 shutdownNow 方法。

這裡推薦使用穩妥的 shutdownNow 來關閉執行緒池,至於更優雅的方式我會在以後的並發編程設計模式中的兩階段終止模式中會再次詳細介紹。

合理的參數

為什麼叫合理的參數,那不合理的參數是什麼樣子的?在我們創建執行緒池的時候,裡面的參數該如何設置才能稱之為合理呢?其實這是有一定的依據的,我們先來看一下以下的創建的方式:

ExecutorService executorService = new ThreadPoolExecutor(5,
                5,
                5,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5),
                r -> {
                    Thread thread = new Thread(r);
                    thread.setName("執行緒池原理講解");
                    return thread;
                });

你說他合理不合理?我也不知道,因為我們沒有參考的依據,在實際的開發中,我們需要根據任務的性質(IO是否頻繁?)來決定我們創建的核心的執行緒數的大小,實際上可以從以下的一個角度來分析:

  • 任務的性質:CPU密集型任務、IO密集型任務和混合型任務;
  • 任務的優先順序:高、中和低;
  • 任務的執行時間:長、中和短;
  • 任務的依賴性:是否依賴其他系統資源,如資料庫連接;

性質不同的任務可以用不同規模的執行緒池分開處理。分為CPU密集型和IO密集型

CPU密集型任務應配置儘可能小的執行緒,如配置 Ncpu+1個執行緒的執行緒池。(可以通過Runtime.getRuntime().availableProcessors()來獲取CPU物理核數)

IO密集型任務執行緒並不是一直在執行任務,則應配置儘可能多的執行緒,如 2*Ncpu

混合型的任務,如果可以拆分,將其拆分成一個CPU密集型任務一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐量將高於串列執行的吞吐量。

如果這兩個任務執行時間相差太大,則沒必要進行分解。可以通過 Runtime.getRuntime().availableProcessors() 方法獲得當前設備的CPU個數。

優先順序不同的任務可以使用優先順序隊列 PriorityBlockingQueue來處理。它可以讓優先順序高的任務先執行(注意:如果一直有優先順序高的任務提交到隊列里,那麼優先順序低的任務可能永遠不能執行

執行時間不同的任務可以交給不同規模的執行緒池來處理,或者可以使用優先順序隊列,讓執行時間短的任務先執行。依賴資料庫連接池的任務,因為執行緒提交SQL後需要等待資料庫返回結果,等待的時間越長,則 CPU 空閑時間就越長,那麼執行緒數應該設置得越大,這樣才能更好地利用CPU。

建議使用有界隊列。有界隊列能增加系統的穩定性和預警能力,可以根據需要設大一點。方式因為提交的任務過多而導致 OOM;

7、本文小結

本文主要介紹的是執行緒池的實現原理以及一些使用技巧,在實際開發中,執行緒池可以說是稍微高級一點的程式設計師的必備技能。所以掌握好執行緒池這門技術也是重中之重!