面試官:爲什麼《阿里巴巴Java開發手冊》上要禁止使用Executors來創建線程池

掃描下方二維碼或者微信搜索公衆號菜鳥飛呀飛,即可關注微信公衆號,閱讀更多Spring源碼分析Java併發編程文章。

微信公衆號

前言

  在《阿里巴巴Java開發手冊》第一章第6講併發處理中,強制規定了線程池不允許使用Executors去創建。那麼爲什麼呢?這就得從線程池和Executors這個類的本質上說起了。

線程池ThreadPoolExecutor

  在Java中提供了兩種類型的線程池來供開發人員使用,分別是ThreadPoolExecutorScheduledThreadPoolExecutor。其中ScheduledThreadPoolExecutor繼承了ThreadPoolExecutor,類的UML圖如下所示。ScheduledThreadPoolExecutor的功能和Java中的Timer類似,它提供了定時的去執行任務或者固定時延的去執行任務的功能,其功能比Timer更加強大。(關於線程池的原理及詳細的源碼分析,可以參考這篇文章:線程池ThreadPoolExecutor的實現原理
線程池UML圖
  線程池有7個非常重要的參數,其描述和功能如下表所示。

參數 功能
int corePoolSize 線程池的核心線程數
int maximumPoolSize 線程池的最大線程數
long keepAliveTime 空閒線程的最大空閒時間
TimeUnit unit 空閒時間的單位,TimeUnit是一個枚舉值,它可以是納秒、微妙、毫秒、秒、分、小時、天
BlockingQueue workQueue 存放任務的阻塞隊列,常用的阻塞隊列有ArrayBolckingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityQueue
ThreadFactory threadFactory 創建線程的工廠,通常利用線程工廠創建線程時,賦予線程具有業務含義的名稱
RejectedExecutionHandler handler 拒絕策略。當線程池線程數超過最大線程數時,線程池無法再接收任務了,這個時候需要執行拒絕策略

  爲什麼說這7個參數十分重要呢?因爲線程池ThreadPoolExecutor的實現原理就是依靠這幾個參數來實現的。當主線程提交一個任務到線程池後,線程池的執行流程如下:

  • 1. 先判斷線程池中線程的數量是否小於核心線程數,即:是否小於corePoolSize,如果小於corePoolSize,就創建新的線程去執行任務;否則就進入到下面流程。
  • 2. 判斷任務隊列是否已經滿了,即:判斷workQueue有沒有滿,如果沒有滿,就將任務添加到任務隊列中;如果已經滿了,就進入到下面的流程。
  • 3. 再判斷如果新創建一個線程後,線程數是否會大於最大線程數,即:是否大於maximumPoolSize,如果大於maximumPoolSize,則進入到下面的流程;否則就創建一個新的線程來執行任務。
  • 4. 執行拒絕策略,即執行handlerrejectedExecution()方法

Executors

  由於ThreadPoolExecutor類的構造方法的參數太多了,創建起來比較麻煩,而且ThreadPoolExecutor又可以細分爲三種類型的線程池,這樣創建起來不太方便。這個時候,工廠設計模式就派上用場了,Executors就是這樣的一個靜態工廠類,它裏面提供了靜態方法,調用這些靜態方法,傳入較少的參數或者不傳參數,我們就可以很輕鬆地創建出線程池。Executors其實就是一個工具類,專門用來創建線程池。
  上面提到ThreadPoolExecutor有7個非常重要的參數,我們在給這些參數傳入特殊的值的時候,創建出來的ThreadPoolExecutor線程池又可以細分爲三類:FixedThreadPool(線程數量固定的線程池)、SingleThreadExecutor(單線程的線程池)、CachedThreadPool(線程數大小無界的線程池)。(注意:這裏的FixedThreadPool、SingleThreadExecutor、CachedThreadPool不是實際的類名,而是根據線程池的特殊性來取的別名)。下面來具體看下這三種線程池。

FixedThreadPool

  FixedThreadPool是線程數量固定的線程池,即核心線程數與最大線程數相等。Executors工廠類提供瞭如下兩個方法去創建FixedThreadPool。

// 指定線程數
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

// 指定線程數和線程工廠
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}

  可以發現,在Executors工廠類中,是直接調用了ThreadPoolExecutor的構造方法,而且令核心線程數和最大線程數均等於傳入的參數nThreads,當線程數量達到核心線程數後,線程數就不會在變化了,始終維持在覈心線程數這個數值,因此這種方法創建出來的線程池稱之爲線程數量固定的線程池。
  同時我們還發現,參數keepAliveTime參數的值被設置爲0,這是因爲當coolPoolSize等於maximumPoolSize時,線程池中始終是不會存在空閒線程的,而keepAliveTime參數的含義是空閒線程存活的最大時間,都不可能出現空閒線程了,設置keepAliveTime的值大於0也就沒有任何意義了,因此這裏將其設置爲0。
  此時任務隊列使用的是LinkedBlockingQueue,由於LinkedBlockingQueue在初始化時,如果不顯示指定大小,就會默認隊列的大小爲Integer.MAX_VALUE,這個數值非常大了,因此通常稱它是一個無界隊列。
當使用無界隊列時,會造成以下問題:

  • 1. 當線程數達到核心線程數後,新添加的任務會被放入到任務隊列中,由於使用無界隊列,那麼就可以無限制的向隊列中添加任務,這有可能造成OOM。同時由於任務隊列中能一直存放任務,那麼就會導致maximunPoolSize這個參數失效。
  • 2. 使用無界隊列,導致線程數不會超過maximunPoolSize,就不會出現空閒線程,也就是將導致keepAliveTime這個參數失效。
  • 3. 使用無界隊列,導致線程數不會超過maximunPoolSize,那麼就永遠不會執行拒絕策略,也就是handler參數失效。
      對於服務器負載較高的應用,由於需要嚴格管控資源,因此在應用中不能隨意創建線程,這個時候適合使用FixedThreadPool,因爲此時線程數固定,只要提前預判好線程數,就不會造成因線程池配置不當而導致服務異常的現象。

SingleThreadExecutor

  SingleThreadExecutor,線程數爲1的線程池,即核心線程數與最大線程數均爲1。Executors工廠類提供瞭如下兩個方法去創建SingleThreadExecutor。

// 不需要傳遞任何參數,在ThreadPoolExecutor的構造方法中,直接令核心線程數和最大線程數爲1
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

// 指定一個線程創建工廠即可,然後在ThreadPoolExecutor的構造方法中,令核心線程數和最大線程數爲1
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}

  從上面代碼中,可以發現,在ThreadPoolExecutor的構造方法中,直接令maximunPoolSize和corePoolSize的值均爲1,這樣線程池中就會一直只存在一個線程,即單線程的線程池。同樣,因爲不會出現存在空閒線程的情況,因此將keepAliveTime設置爲0。任務隊列依然使用的是LinekdBlockingQueue,即無界隊列。由於使用無界隊列,因此仍然可能會造成OOM異常,以及keepAliveTime、maximunPoolSize、handler等參數失效。
  對於需要保證任務順序執行的場景,可以使用SingleThreadExecutor。

CachedThreadPool

  CachedThreadPool,線程數大小無界的線程池。核心線程數等於0,最大線程數等於Integer.MAX_VALUE,這個值已經非常大了,因此稱之爲線程數大小無界的線程池。Executors工廠類提供瞭如下兩個方法去創建CachedThreadPool。

// 不要傳任何參數,在ThreadPoolExecutor的構造方法中,令核心線程數爲0,最大線程數爲Integer.MAX_VALUE
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

// 指定線程創建工廠,然後在ThreadPoolExecutor的構造方法中,令核心線程數爲0,最大線程數爲Integer.MAX_VALUE
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}

  從上面的代碼中可以發現,在ThreadPoolExecutor的構造方法中,令核心線程數爲0,最大線程數爲Integer.MAX_VALUE,由於Integer.MAX_VALUE的值非常大,因此通常也稱CachedThreadPool爲線程數大小無界的線程池。令keepAliveTime等於60,單位爲秒,這說明空閒線程最多存活60秒。
  在CachedPoolPool中,使用的阻塞隊列不再是LinkedBlockingQueue,而是SynchronousQueue,這是一個不存儲元素的阻塞隊列。它的特點是,當前線程向隊列中put元素時,必須要等另外一個線程從隊列中取出元素後,當前線程纔會返回;如果沒有線程從隊列中取出元素,那麼當前線程就會一直阻塞,直到元素被取出。因此稱SynchronousQueue是一個不存儲元素的隊列。(注意:這裏說的是put操作會阻塞,而offer操作是不阻塞的)
  由於核心線程數爲0,所以當有任務提交到線程池時,第一層判斷不成立(即當前線程數小於核心線程數判斷不成立,此時均爲0)。因此會調用阻塞隊列的offer()方法嘗試將任務添加到任務隊列中,由於此時的阻塞隊列是SynchronousQueue,它不存儲元素,因此offer()方法會返回false,這樣就表示第二層判斷不成立(任務無法添加到隊列)。就接着判斷當前線程數是否大於最大線程數,顯然此時沒有,因爲最大線程數爲Integer.MAX_VALUE,所以此時會創建新的線程去處理任務。這樣只要當有新的任務進入到池中時,就會創建新的線程去處理任務,因此稱CachedThreadPool是一個線程數無界的線程池。池中的線程最多空閒60秒,當60秒內沒有從阻塞隊列中獲取到任務後,線程就會被銷燬。當主線程提交任務的速度大於線程池處理任務的速度時,線程池就會一直創建線程,因此最終有可能造成OOM異常。
  當任務較多,但任務執行時間較短時,適合使用CacheThreadPool這種線程池來處理任務。

  JUC包下還提供了一種很常用的線程池,它就是ScheduledThreadPoolExecutor。ScheduledThreadPoolExecutor是ThreadPoolExecutor的子類,它的功能是定期執行任務或者在給定的延時之後執行任務。將線程池的核心參數設置爲特殊的值,就會創建出兩種類型的ScheduledThreadPoolExecutor。分別是包含多個線程的ScheduledThreadPoolExecutor和只包含一個線程的SingleScheduledThreadExecutor。(注意:SingleScheduledThreadExecutor不是一個類名,而是根據線程池的特性來取的一個名稱)。
  同樣,Executors靜態工廠類也爲ScheduledThreadPoolExecutor的創建提供了相關的靜態方法。下面結合代碼示例來分別分析兩種類型的ScheduledThreadPoolExecutor。

多個線程的ScheduledThreadPoolExecutor

  當ScheduledThreadPoolExecutor的核心線程數指定爲多個時(大於1),ScheduledThreadPoolExecutor就是多線程的線程池。Executors工廠類提供瞭如下兩個方法去創建多個線程的ScheduledThreadPoolExecutor。

// 指定核心線程數的數量
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

// 指定核心線程數的數量和線程工廠
public static ScheduledExecutorService newScheduledThreadPool(
        int corePoolSize, ThreadFactory threadFactory) {
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}

  當傳入的參數corePoolSize大於1時,就是多線程的ScheduledThreadPoolExecutor,當傳入的數值等於1時,就變成了單線程的SingleThreadScheduledExecutor。下面來看下ScheduledThreadPoolExecutor帶有一個參數的構造方法。源碼如下:

public ScheduledThreadPoolExecutor(int corePoolSize) {
	// 核心線程數爲傳入的線程數,即1
	// 最大線程數爲Integer.MAX_VALUE
	// 使用的阻塞隊列是DelayedWorkQueue,這是一個無界隊列
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

  可以發現,ScheduledThreadPoolExecutor的最大線程數爲Integer.MAX_VALUE,使用的是DelayedWorkQueue隊列,這是一個無界隊列。由於是無界隊列,那麼就會是最大線程數maximumPoolSize這個參數無效,所以即使將最大線程數爲Integer.MAX_VALUE也沒有什麼用處。
  DelayedWorkQueue又是一個什麼隊列呢?它是ScheduledThreadPoolExecutor定義的一個靜態內部類,它的本質就是一個延時隊列,其功能和DelayQueue類似。在DelayQueue中,包含了一個PriorityQueue(具有優先級的隊列)類型的屬性,而DelayedWorkQueue是DelayQueue和PriorityQueue的結合體,它會將提交到線程池的任務封裝成一個RunnableScheduledFuture對象,然後將這些對象按照一定規則排好序
  RunnableScheduledFuture是ScheduledThreadPoolExecutor的一個私有內部類,繼承了FutureTask。它包含三個非常重要的屬性:

  • 1. sequenceNumber,任務被添加到線程池時的序號
  • 2. time,任務在哪個時間點執行
  • 3. period,任務執行的週期

  DelayedWorkQueue會將隊列中所有的RunnableScheduledFuture按照每個RunnableScheduledFuture的time按照從小到大排序,時間最小的應該最先被執行,所以排在最前面,當出現多個任務的時間相同時,就按照sequenceNumber這個序號從小到大排序,這樣線程池中就能定時的執行這些任務了。

ScheduledThreadPoolExecutor執行任務的詳細步驟如下:

  • 1. 從DelayedWorkQueue隊列中通過peek()獲取第一個任務,判斷任務的執行時間是否小於當前時間,如果不小於,則說明還沒到任務的執行時間,就讓線程再繼續等待一段時間;如果小於或者等於,就執行下面的流程。
  • 2. 通過poll()操作從隊列中取出第一個任務,如果隊列中還有任務,就喚醒處於等待隊列中的線程,通知它們也來嘗試獲取任務。
  • 3. 當前線程執行取出的任務。
  • 4. 執行完任務後,修改RunnableScheduledFuture任務的time屬性的值,將其設置爲下次將要在被執行的時間點,然後將任務放回到任務隊列中。

SingleScheduledThreadExecutor

  SingleThreadScheduledExecutor指的是線程池只有一個線程的ScheduledThreadPoolExecutor,此時核心線程數爲1。
  Executors工廠類提供瞭如下兩個方法去創建SingleScheduledThreadExecutor。

// 不需要傳任何參數,直接指定核心線程數爲1
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
    // 將ScheduledThreadPoolExecutor包裝成了DelegatedScheduledExecutorService
    return new DelegatedScheduledExecutorService
        (new ScheduledThreadPoolExecutor(1));
}

// 傳入線程工廠,然後指定核心線程數爲1
public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
    return new DelegatedScheduledExecutorService
        (new ScheduledThreadPoolExecutor(1, threadFactory));
}

SingleScheduledThreadExecutor執行任務的邏輯和多線程的ScheduledThreadPoolExecutor一樣。唯一的區別就是它只有一個線程來執行任務,因此它能保證任務的執行順序,適用於需要保證任務按照順序執行的場景。

總結

  • 本文詳細介紹了線程池的幾種類型,普通線程池ThreadPoolExecutor根據設置的核心參數的不同,可以細分爲三類線程池:固定線程的線程池(FixedThreadPool)、單線程的線程池(SingleThreadExecutor)、線程數無界的線程池(CachedThreadPool);對於定時任務類型的線程池ScheduledThreadPoolExecutor也可以根據核心參數的不同設置,可以細分爲兩類:多線程的ScheduledThreadPoolExecutor和單線程的SingleThreadScheduledExecutor,兩者唯一的區別就是線程池中的線程數量不一樣。
  • 同時介紹了靜態工廠類Executors的使用,以及如何利用它來創建文中提到的幾種線程池。
  • 最後,回到本文的開頭,爲什麼《阿里巴巴Java開發手冊》上要禁止使用Executors來創建線程池?Executors這個靜態工廠類這麼好用,創建線程池的時候特別方便,我們不用指定很多參數,就能創建出一個線程池,爲什麼要禁止呢?答案就是Executors創建出來的線程池使用的全都是無界隊列,而使用無界隊列會帶來很多弊端,最重要的就是,它可以無限保存任務,因此很有可能造成OOM異常。同時在某些類型的線程池裏面,使用無界隊列還會導致maxinumPoolSize、keepAliveTime、handler等參數失效。因此目前在大廠的開發規範中會強調禁止使用Executors來創建線程池。
  • 這個問題的答案其實很簡單,關鍵之處在於掌握線程池的7個重要的核心參數,以及明白線程池的原理以及每一個參數的意義。瞭解了它們的原理,無論是對於面試,還是平時工作中的使用,以及排查問題都有很大的幫助。

推薦

微信公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章