你都理解建立執行緒池的引數嗎?

後端進階發表於2019-04-11

多執行緒可以說是面試官最喜歡拿來問的題目之一了,可謂是老生之常談,不管你是新手還是老司機,我相信你一定會在面試過程中遇到過有關多執行緒的一些問題。那我現在就充當一次面試官,我來問你:

現有一個執行緒池,引數corePoolSize = 5,maximumPoolSize = 10,BlockingQueue阻塞佇列長度為5,此時有4個任務同時進來,問:執行緒池會建立幾條執行緒?

如果4個任務還沒處理完,這時又同時進來2個任務,問:執行緒池又會建立幾條執行緒還是不會建立?

如果前面6個任務還是沒有處理完,這時又同時進來5個任務,問:執行緒池又會建立幾條執行緒還是不會建立?

如果你此時一臉懵逼,請不要慌,問題不大。

dont_panic

建立執行緒池的構造方法的引數都有哪些?

要回答這個問題,我們需要從建立執行緒池的引數去找答案:

java.util.concurrent.ThreadPoolExecutor#ThreadPoolExecutor:

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;
}
複製程式碼

建立執行緒池一共有7個引數,從原始碼可知,corePoolSize和maximumPoolSize都不能小於0,且核心執行緒數不能大於最大執行緒數。

下面我來解釋一下這7個引數的用途:

corePoolSize

執行緒池核心執行緒數量,核心執行緒不會被回收,即使沒有任務執行,也會保持空閒狀態。

maximumPoolSize

池允許最大的執行緒數,當執行緒數量達到corePoolSize,且workQueue佇列塞滿任務了之後,繼續建立執行緒。

keepAliveTime

超過corePoolSize之後的“臨時執行緒”的存活時間。

unit

keepAliveTime的單位。

workQueue

當前執行緒數超過corePoolSize時,新的任務會處在等待狀態,並存在workQueue中,BlockingQueue是一個先進先出的阻塞式佇列實現,底層實現會涉及Java併發的AQS機制,有關於AQS的相關知識,我會單獨寫一篇,敬請期待。

threadFactory

建立執行緒的工廠類,通常我們會自頂一個threadFactory設定執行緒的名稱,這樣我們就可以知道執行緒是由哪個工廠類建立的,可以快速定位。

handler

執行緒池執行拒絕策略,當線數量達到maximumPoolSize大小,並且workQueue也已經塞滿了任務的情況下,執行緒池會呼叫handler拒絕策略來處理請求。

系統預設的拒絕策略有以下幾種:

  1. AbortPolicy:為執行緒池預設的拒絕策略,該策略直接拋異常處理。
  2. DiscardPolicy:直接拋棄不處理。
  3. DiscardOldestPolicy:丟棄佇列中最老的任務。
  4. CallerRunsPolicy:將任務分配給當前執行execute方法執行緒來處理。

我們還可以自定義拒絕策略,只需要實現RejectedExecutionHandler介面即可,友好的拒絕策略實現有如下:

  1. 將資料儲存到資料,待系統空閒時再進行處理
  2. 將資料用日誌進行記錄,後由人工處理

現在我們回到剛開始的問題就很好回答了:

執行緒池corePoolSize=5,執行緒初始化時不會自動建立執行緒,所以當有4個任務同時進來時,執行execute方法會新建【4】條執行緒來執行任務;

前面的4個任務都沒完成,現在又進來2個佇列,會新建【1】條執行緒來執行任務,這時poolSize=corePoolSize,還剩下1個任務,執行緒池會將剩下這個任務塞進阻塞佇列中,等待空閒執行緒執行;

如果前面6個任務還是沒有處理完,這時又同時進來了5個任務,此時還沒有空閒執行緒來執行新來的任務,所以執行緒池繼續將這5個任務塞進阻塞佇列,但發現阻塞佇列已經滿了,核心執行緒也用完了,還剩下1個任務不知道如何是好,於是執行緒池只能建立【1】條“臨時”執行緒來執行這個任務了;

這裡建立的執行緒用“臨時”來描述還是因為它們不會長期存在於執行緒池,它們的存活時間為keepAliveTime,此後執行緒池會維持最少corePoolSize數量的執行緒。

你都理解建立執行緒池的引數嗎?

為什麼不建議使用Executors建立執行緒池?

JDK為我們提供了Executors執行緒池工具類,裡面有預設的執行緒池建立策略,大概有以下幾種:

  1. FixedThreadPool:執行緒池執行緒數量固定,即corePoolSize和maximumPoolSize數量一樣。
  2. SingleThreadPool:單個執行緒的執行緒池。
  3. CachedThreadPool:初始核心執行緒數量為0,最大執行緒數量為Integer.MAX_VALUE,執行緒空閒時存活時間為60秒,並且它的阻塞佇列為SynchronousQueue,它的初始長度為0,這會導致任務每次進來都會建立執行緒來執行,線上程空閒時,存活時間到了又會釋放執行緒資源。
  4. ScheduledThreadPool:建立一個定長的執行緒池,而且支援定時的以及週期性的任務執行,類似於Timer。

用Executors工具類雖然很方便,我依然不推薦大家使用以上預設的執行緒池建立策略,阿里巴巴開發手冊也是強制不允許使用Executors來建立執行緒池,我們從JDK原始碼中尋找一波答案:

java.util.concurrent.Executors:

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

// SingleThreadPool
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

// CachedThreadPool
public static ExecutorService newCachedThreadPool() {
    // 允許建立執行緒數為Integer.MAX_VALUE
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

// ScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
    // 允許建立執行緒數為Integer.MAX_VALUE
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
複製程式碼
public LinkedBlockingQueue() {
    // 允許佇列長度最大為Integer.MAX_VALUE
    this(Integer.MAX_VALUE);
}
複製程式碼

從JDK原始碼可看出,Executors工具類無非是把一些特定引數進行了封裝,並提供一些方法供我們呼叫而已,我們並不能靈活地填寫引數,策略過於簡單,不夠友好

CachedThreadPool和ScheduledThreadPool最大執行緒數為Integer.MAX_VALUE,如果執行緒無限地建立,會造成OOM異常。

LinkedBlockingQueue基於連結串列的FIFO佇列,是無界的,預設大小是Integer.MAX_VALUE,因此FixedThreadPool和SingleThreadPool的阻塞佇列長度為Integer.MAX_VALUE,如果此時佇列被無限地堆積任務,會造成OOM異常。

公眾號「後端進階」,專注後端技術分享!

相關文章