Java執行緒池二:執行緒池原理

油多壞不了菜發表於2020-12-20

最近精讀Netty原始碼,讀到NioEventLoop部分的時候,發現對Java執行緒&執行緒池有些概念還有困惑, 所以深入總結一下
Java執行緒池一:執行緒基礎
Java執行緒池二:執行緒池原理

為什麼需要使用執行緒池

Java執行緒對映的是系統核心執行緒,是稀缺資源,使用執行緒池主要有以下幾點好處

  • 降低資源消耗:重複利用池中執行緒降低執行緒的建立和消耗造成的資源消耗。
  • 提高響應速度:任務到達時直接使用池總中空閒的執行緒,可以不用等待執行緒建立。
  • 提高執行緒的可管理性:執行緒是稀缺資源,不能無限制建立,使用執行緒池可以統一進行分配、監控、調優。

執行緒池框架簡介

  • Executor介面:提供execute方法提交任務
  • ExecutorService介面:提供可以跟蹤任務執行結果的 submit方法 & 提供執行緒池關閉的方法(shutdown, shutdowNow)
  • AbstractExecutorService抽象類:實現submit方法
  • ThreadPoolExecutor: 執行緒池實現類
  • ScheduleThreadPoolExecutor:可以執行定時任務的執行緒池

ThreadPoolExecutor原理

核心引數以及含義

  • corePoolSize:核心執行緒池大小
  • maximumPoolSize: 執行緒池最大大小
  • workQueue: 工作佇列(任務暫時存放的地方)
  • RejectedExecutionHandler:拒絕策略(執行緒池無法執行該任務時的處理策略)

任務提交流程

任務提交過程見下流程圖

執行緒池的狀態

  • RUNNING:正常的執行緒池執行狀態
  • SHUTDOWN:呼叫shutdown方法到該狀態,該狀態下拒絕提交新任務,但會將已提交的任務的處理完畢
  • STOP:呼叫shutdownNow方法到該狀態,該狀態下拒絕新任務的提交 & 丟棄工作佇列中的任務 & 中斷正在執行任務的工作執行緒
  • TIDYING:工作佇列和執行緒池都為空時自動到該狀態
  • TERMINATED:terminated方法返回之後自動到該狀態

工作佇列

核心執行緒池滿時,任務會嘗試提交到工作佇列,後續工作執行緒會從工作佇列中獲取任務執行。

因為涉及到多個執行緒對工作佇列的讀寫,所以工作佇列需要是執行緒安全的,Java提供了以下幾種執行緒安全的佇列(BlockingQueue)

實現類 工作機制
ArrayBlockingQueue 底層實現是陣列
LinkedBlockingDeque 底層實現是連結串列
PriorityBlockingQueue 優先佇列,本質是個小頂堆
DelayQueue 延時佇列 (優先佇列 & 元素實現Delayed介面),ScheduledThreadPoolExecutor實現的關鍵
SynchronousQueue 同步佇列

BlockingQueue 多組讀寫操作API

操作 描述
add/remove 佇列已滿/佇列已空時,丟擲異常
put/take 佇列已滿/佇列已空時,阻塞等待
offer/poll 佇列已滿/佇列已空時,返回特殊值(false/null)
offer(time) / poll(time) 超時時間內無法寫入或者讀取成功,返回特殊值

拒絕策略

拒絕策略是當執行緒池滿負載時(任務佇列已滿 & 執行緒池已滿)對新提交任務的處理策略,jdk提供瞭如下四種實現,其中AbortPolicy是預設實現。

實現類 工作機制
AbortPolicy 丟擲RejectedExecutionException異常
CallerRunsPolicy 呼叫執行緒執行該任務
DiscardOldestPolicy 丟棄工作佇列頭部任務,再嘗試提交該任務
DiscardPolicy 直接丟棄

當然我們可以有自定義的實現,比如記錄日誌、任務例項持久化,同時傳送報警到開發人員。

跟蹤任務的執行結果

執行緒池提供了幾個submit方法, 呼叫執行緒可以根據返回的Future物件獲取任務執行結果,那麼它的實現原理又是什麼吶?

裝飾模式對task的run方法進行增強

1.提交任務前,會把task裝飾成一個FutureTask物件

 public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
 }

2.FutureTask物件的run方法會儲存返回的結果或者異常。呼叫方可以根據FutureTask獲取任務的執行結果。

//省略了部分程式碼
public void run() {
      Callable<V> c = callable;
      if (c != null && state == NEW) {
        V result;
        boolean ran;
        try {
          //執行任務
          result = c.call();
          ran = true;
        } catch (Throwable ex) {
          result = null;
          ran = false;
          //儲存異常
          setException(ex);
        }
        if (ran)
          //儲存返回值
          set(result);
 }

執行緒池的關閉

shutdown

shutdown將執行緒池的狀態設定成SHUTDOWN,同時拒絕提交新的任務,但是已提交的任務會正常執行

shutdownNow

shutdownNow將執行緒池的狀態設定成STOP,該狀態下拒絕提交新的任務 & 丟棄工作佇列中的任務& 中斷當前活躍的執行緒(嘗試停止正在執行的任務)

需要注意的是shutdownNow對於正在執行的任務只是嘗試停止,不保證成功(取決於任務是否監聽處理中斷位)

ScheduledThreadPoolExecutor 定時排程原理

ScheduledThreadPoolExecutor在ThreadPoolExecutor之上擴充套件實現了定時排程的能力

1.例項化時工作佇列使用延時佇列(DelayedWorkQueue)--- 本質是個小頂堆

public ScheduledThreadPoolExecutor(int corePoolSize,
                                   RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), handler);
}

2.提交的任務裝飾成ScheduledFutureTask型別,並把任務加入到工作佇列(不直接呼叫execute)

public ScheduledFuture<?> schedule(Runnable command,
                                       long delay,
                                       TimeUnit unit) {
        if (command == null || unit == null)
            throw new NullPointerException();
        //裝飾
        RunnableScheduledFuture<?> t = decorateTask(command,
            new ScheduledFutureTask<Void>(command, null,
                                          triggerTime(delay, unit)));
  			//任務加入工作佇列
        delayedExecute(t);
        return t;
    }

3.ScheduledFutureTask實現Delayed和Comparable介面

所以提交到工作佇列中的任務是按照任務執行時間排序的(最早執行的任務在頭部),因為工作佇列是個小頂堆。

public long getDelay(TimeUnit unit) {
    return unit.convert(time - now(), NANOSECONDS);
}

public int compareTo(Delayed other) {
    if (other == this) // compare zero if same object
        return 0;
    if (other instanceof ScheduledFutureTask) {
        ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
        long diff = time - x.time;
        if (diff < 0)
            return -1;
        else if (diff > 0)
            return 1;
        else if (sequenceNumber < x.sequenceNumber)
            return -1;
        else
            return 1;
    }
    long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
    return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
}

4.只能從工作佇列中獲取已到執行時間的任務

public RunnableScheduledFuture<?> poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        RunnableScheduledFuture<?> first = queue[0];
      	//如果頭部的任務還沒有到執行時間, 直接返回null
        if (first == null || first.getDelay(NANOSECONDS) > 0)
            return null;
        else
            return finishPoll(first);
    } finally {
        lock.unlock();
    }
}

執行緒池配置

假設:CPU核心數是N,每個任務的執行時間是T,任務的超時時間是timeout,核心執行緒數是corePoolSize,工作佇列大小是workQueue, 最大執行緒數是 maxPoolSize, 任務最大併發數為maxTasks

核心執行緒數配置

  1. 對於CPU密集型任務:corePoolSize 大小設定成和CPU核心數接近,如N+1 或者 N+2

  2. 對於IO密集型任務:corePoolSize可以設定的比較大一些,如2N~3N;也可以通過如下邏輯進行估算

    假設80%的時間是IO操作,那麼每個任務需要佔用CPU時間大概是0.2T, 每秒每個CPU核心最大可以執行的任務數為 = (1/0.2T) = 5/T;所以理論上 80%IO的情況下corePoolSize可以設定為 5N (一個cpu可以對應5個工作執行緒)

工作佇列大小配置

工作佇列的大小取決於任務的超時時間 & 核心執行緒池的吞吐量

workQueue = corePoolSize * (1/T) * timeout = (corePoolSize * timeout) / T

需要注意的是: 工作佇列不能使用無界佇列。(無界佇列異常情況下可能耗盡系統資源,造成服務不可用)

最大執行緒數配置

最大執行緒數的大小取決於最大的任務併發數 & 工作佇列的大小 & 任務的執行時間

maxPoolSize = (maxTasks - workQueue) / T

拒絕策略配置

對於無關緊要的任務,我們可以直接丟棄;對於一些重要的任務需要對任務進行持久化,以便後續進行補償和恢復。

執行緒池監控

我們可以有個定時指令碼將執行緒池的最大執行緒數、工作佇列大小、已經執行的任務數、已經拒絕的任務數等資料推送到監控系統

這樣我們可以根據這些資料對執行緒池進行調優,也可以即使感知線上業務異常。

相關文章