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

掃描下方二維碼或者微信搜索公衆號菜鳥飛呀飛,便可關注微信公衆號,閱讀更多Spring源碼分析Java併發編程文章。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個重要的核心參數,以及明白線程池的原理以及每個參數的意義。瞭解了它們的原理,不管是對於面試,仍是平時工做中的使用,以及排查問題都有很大的幫助。

推薦

微信公衆號
相關文章
相關標籤/搜索