從原始碼的角度解析執行緒池執行原理

Java科代表發表於2019-04-25

在講解完執行緒池的構造引數和一些不常用的設定之後,有些同學還是想繼續深入地瞭解執行緒池的原理,所以這篇文章科代表會帶大家深入原始碼,從底層吃透執行緒池的執行原理。

從原始碼的角度解析執行緒池執行原理

ThreadPoolExecutor

在深入原始碼之前先來看看J.U.C包中的執行緒池類圖:

從原始碼的角度解析執行緒池執行原理

它們的最頂層是一個Executor介面,它只有一個方法:

public interface Executor {
    void execute(Runnable command);
}
複製程式碼

它提供了一個執行新任務的簡單方法,Java執行緒池也稱之為Executor框架。

ExecutorService擴充套件了Executor,新增了操控執行緒池生命週期的方法,如shutDown(),shutDownNow()等,以及擴充套件了可非同步跟蹤執行任務生成返回值Future的方法,如submit()等方法。

ThreadPoolExecutor繼承自AbstractExecutorService,同時實現了ExecutorService介面,也是Executor框架預設的執行緒池實現類,也是這篇文章重點分析的物件,一般我們使用執行緒池,如沒有特殊要求,直接建立ThreadPoolExecutor,初始化一個執行緒池,如果需要特殊的執行緒池,則直接繼承ThreadPoolExecutor,並實現特定的功能,如ScheduledThreadPoolExecutor,它是一個具有定時執行任務的執行緒池。

下面我們開始ThreadPoolExecutor的原始碼分析了(以下原始碼為JDK8版本):

ctl變數

ctl是一個Integer值,它是對執行緒池執行狀態和執行緒池中有效執行緒數量進行控制的欄位,Integer值一共有32位,其中高3位表示"執行緒池狀態",低29位表示"執行緒池中的任務數量"。我們看看Doug Lea大神是如何實現的:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

// Packing and unpacking ctl
// 通過位運算獲取執行緒池執行狀態
private static int runStateOf(int c)     { return c & ~CAPACITY; }
// 通過位運算獲取執行緒池中有效的工作執行緒數
private static int workerCountOf(int c)  { return c & CAPACITY; }
// 初始化ctl變數值
private static int ctlOf(int rs, int wc) { return rs | wc; }
複製程式碼

執行緒池一共有狀態5種狀態,分別是:

  1. Running:執行緒池初始化時預設的狀態,表示執行緒正處於執行狀態,能夠接受新提交的任務,同時也能夠處理阻塞佇列中的任務;
  2. SHUTDOWN:呼叫shutdown()方法會使執行緒池進入到該狀態,該狀態下不再繼續接受新提交的任務,但是還會處理阻塞佇列中的任務;
  3. STOP:呼叫shutdownNow()方法會使執行緒池進入到該狀態,該狀態下不再繼續接受新提交的任務,同時不再處理阻塞佇列中的任務;
  4. TIDYING:如果執行緒池中workerCount=0,即有效執行緒數量為0時,會進入該狀態;
  5. TERMINATED:在terminated()方法執行完後進入該狀態,只不過terminated()方法需要我們自行實現。

我們再來看看位運算:

COUNT_BITS表示ctl變數中表示有效執行緒數量的位數,這裡COUNT_BITS=29;

CAPACITY表示最大有效執行緒數,根據位運算得出COUNT_MASK=11111111111111111111111111111,這算成十進位制大約是5億,在設計之初就已經想到不會開啟超過5億條執行緒,所以完全夠用了;

執行緒池狀態的位運算得到以下值:

  1. RUNNING:高三位值111
  2. SHUTDOWN:高三位值000
  3. STOP:高三位值001
  4. TIDYING:高三位值010
  5. TERMINATED:高三位值011

這裡簡單解釋一下Doug Lea大神為什麼使用一個Integer變數表示兩個值:

很多人會想,一個變數表示兩個值,就節省了儲存空間,但是這裡很顯然不是為了節省空間而設計的,即使將這輛個值拆分成兩個Integer值,一個執行緒池也就多了4個位元組而已,為了這4個位元組而去大費周章地設計一通,顯然不是Doug Lea大神的初衷。

在多執行緒的環境下,執行狀態和有效執行緒數量往往需要保證統一,不能出現一個改而另一個沒有改的情況,如果將他們放在同一個AtomicInteger中,利用AtomicInteger的原子操作,就可以保證這兩個值始終是統一的。

Doug Lea大神牛逼!

從原始碼的角度解析執行緒池執行原理

Worker

Worker類繼承了AQS,並實現了Runnable介面,它有兩個重要的成員變數:firstTask和thread。firstTask用於儲存第一次新建的任務;thread是在呼叫構造方法時通過ThreadFactory來建立的執行緒,是用來處理任務的執行緒。

如何線上程池中新增任務?

執行緒池要執行任務,那麼必須先新增任務,execute()雖說是執行任務的意思,但裡面也包含了新增任務的步驟在裡面,下面原始碼:

java.util.concurrent.ThreadPoolExecutor#execute:

public void execute(Runnable command) {
  // 如果新增訂單任務為空,則空指標異常
  if (command == null)
    throw new NullPointerException();
  // 獲取ctl值
  int c = ctl.get();
  // 1.如果當前有效執行緒數小於核心執行緒數,呼叫addWorker執行任務(即建立一條執行緒執行該任務)
  if (workerCountOf(c) < corePoolSize) {
    if (addWorker(command, true))
      return;
    c = ctl.get();
  }
  // 2.如果當前有效執行緒大於等於核心執行緒數,並且當前執行緒池狀態為執行狀態,則將任務新增到阻塞佇列中,等待空閒執行緒取出佇列執行
  if (isRunning(c) && workQueue.offer(command)) {
    int recheck = ctl.get();
    if (! isRunning(recheck) && remove(command))
      reject(command);
    else if (workerCountOf(recheck) == 0)
      addWorker(null, false);
  }
  // 3.如果阻塞佇列已滿,則呼叫addWorker執行任務(即建立一條執行緒執行該任務)
  else if (!addWorker(command, false))
    // 如果建立執行緒失敗,則呼叫執行緒拒絕策略
    reject(command);
}
複製程式碼

可以發現,原始碼的解讀對應「你都瞭解執行緒池的引數嗎?」裡面那道面試題的解析是一樣的,我在這裡畫一下execute執行任務的流程圖:

從原始碼的角度解析執行緒池執行原理

繼續往下看,addWorker新增任務,方法原始碼有點長,我按照邏輯拆分成兩部分講解:

java.util.concurrent.ThreadPoolExecutor#addWorker:

retry:
for (;;) {
  int c = ctl.get();
  // 獲取執行緒池當前執行狀態
  int rs = runStateOf(c);

  // 如果rs大於SHUTDOWN,則說明此時執行緒池不在接受新任務了
  // 如果rs等於SHUTDOWN,同時滿足firstTask為空,且阻塞佇列如果有任務,則繼續執行任務
  // 也就說明了如果執行緒池處於SHUTDOWN狀態時,可以繼續執行阻塞佇列中的任務,但不能繼續往執行緒池中新增任務了
  if (rs >= SHUTDOWN &&
      ! (rs == SHUTDOWN &&
         firstTask == null &&
         ! workQueue.isEmpty()))
    return false;

  for (;;) {
    // 獲取有效執行緒數量
    int wc = workerCountOf(c);
    // 如果有效執行緒數大於等於執行緒池所容納的最大執行緒數(基本不可能發生),不能新增任務
    // 或者有效執行緒數大於等於當前限制的執行緒數,也不能新增任務
    // 限制執行緒數量有任務是否要核心執行緒執行決定,core=true使用核心執行緒執行任務
    if (wc >= CAPACITY ||
        wc >= (core ? corePoolSize : maximumPoolSize))
      return false;
    // 使用AQS增加有效執行緒數量
    if (compareAndIncrementWorkerCount(c))
      break retry;
    // 如果再次獲取ctl變數值
    c = ctl.get();  // Re-read ctl
    // 再次對比執行狀態,如果不一致,再次迴圈執行
    if (runStateOf(c) != rs)
      continue retry;
    // else CAS failed due to workerCount change; retry inner loop
  }
}
複製程式碼

這裡特別強調,firstTask是開啟執行緒執行的首個任務,之後常駐線上程池中的執行緒執行的任務都是從阻塞佇列中取出的,需要注意。

以上for迴圈程式碼主要作用是判斷ctl變數當前的狀態是否可以新增任務,特別說明了如果執行緒池處於SHUTDOWN狀態時,可以繼續執行阻塞佇列中的任務,但不能繼續往執行緒池中新增任務了;同時增加工作執行緒數量使用了AQS作同步,如果同步失敗,則繼續迴圈執行。

// 任務是否已執行
boolean workerStarted = false;
// 任務是否已新增
boolean workerAdded = false;
// 任務包裝類,我們的任務都需要新增到Worker中
Worker w = null;
try {
  // 建立一個Worker
  w = new Worker(firstTask);
  // 獲取Worker中的Thread值
  final Thread t = w.thread;
  if (t != null) {
    // 操作workers HashSet 資料結構需要同步加鎖
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
      // Recheck while holding lock.
      // Back out on ThreadFactory failure or if
      // shut down before lock acquired.
      // 獲取當前執行緒池的執行狀態
      int rs = runStateOf(ctl.get());
      // rs < SHUTDOWN表示是RUNNING狀態;
      // 如果rs是RUNNING狀態或者rs是SHUTDOWN狀態並且firstTask為null,向執行緒池中新增執行緒。
      // 因為在SHUTDOWN時不會在新增新的任務,但還是會執行workQueue中的任務
      // rs是RUNNING狀態時,直接建立執行緒執行任務
      // 當rs等於SHUTDOWN時,並且firstTask為空,也可以建立執行緒執行任務,也說說明了SHUTDOWN狀態時不再接受新任務
      if (rs < SHUTDOWN ||
          (rs == SHUTDOWN && firstTask == null)) {
        if (t.isAlive()) // precheck that t is startable
          throw new IllegalThreadStateException();
        workers.add(w);
        int s = workers.size();
        if (s > largestPoolSize)
          largestPoolSize = s;
        workerAdded = true;
      }
    } finally {
      mainLock.unlock();
    }
    // 啟動執行緒執行任務
    if (workerAdded) {
      t.start();
      workerStarted = true;
    }
  }
} finally {
  if (! workerStarted)
    addWorkerFailed(w);
}
return workerStarted;
}
複製程式碼

以上原始碼主要的作用是建立一個Worker物件,並將新的任務裝進Worker中,開啟同步將Worker新增進workers中,這裡需要注意workers的資料結構為HashSet,非執行緒安全,所以操作workers需要加同步鎖。新增步驟做完後就啟動執行緒來執行任務了,繼續往下看。

如何執行任務?

我們注意到上面的程式碼中:

// 啟動執行緒執行任務
if (workerAdded) {
  t.start();
  workerStarted = true;
}
複製程式碼

這裡的t是w.thread得到的,即是Worker中用於執行任務的執行緒,該執行緒由ThreadFactory建立,我們再看看生成Worker的構造方法:

Worker(Runnable firstTask) {
  setState(-1); // inhibit interrupts until runWorker
  this.firstTask = firstTask;
  this.thread = getThreadFactory().newThread(this);
}
複製程式碼

newThread傳的引數是Worker本身,而Worker實現了Runnable介面,所以當我們執行t.start()時,執行的是Worker的run()方法,找到入口了:

java.util.concurrent.ThreadPoolExecutor.Worker#run:

public void run() {
  runWorker(this);
}
複製程式碼

java.util.concurrent.ThreadPoolExecutor#runWorker:

final void runWorker(Worker w) {
  Thread wt = Thread.currentThread();
  Runnable task = w.firstTask;
  w.firstTask = null;
  w.unlock(); // allow interrupts
  boolean completedAbruptly = true;
  try {
    // 迴圈從workQueue阻塞佇列中獲取任務並執行
    while (task != null || (task = getTask()) != null) {
      // 加同步鎖的目的是為了防止同一個任務出現多個執行緒執行的問題
      w.lock();
      // 如果執行緒池正在關閉,須確保中斷當前執行緒
      if ((runStateAtLeast(ctl.get(), STOP) ||
           (Thread.interrupted() &&
            runStateAtLeast(ctl.get(), STOP))) &&
          !wt.isInterrupted())
        wt.interrupt();
      try {
        // 執行任務前可以做一些操作
        beforeExecute(wt, task);
        Throwable thrown = null;
        try {
          // 執行任務
          task.run();
        } catch (RuntimeException x) {
          thrown = x; throw x;
        } catch (Error x) {
          thrown = x; throw x;
        } catch (Throwable x) {
          thrown = x; throw new Error(x);
        } finally {
          // 執行任務後可以做一些操作
          afterExecute(task, thrown);
        }
      } finally {
        // 將task置為空,讓執行緒自行呼叫getTask()方法從workQueue阻塞佇列中獲取任務
        task = null;
        // 記錄Worker執行了多少次任務
        w.completedTasks++;
        w.unlock();
      }
    }
    completedAbruptly = false;
  } finally {
    // 執行緒回收過程
    processWorkerExit(w, completedAbruptly);
  }
}
複製程式碼

這一步是執行任務的核心方法,首次執行不為空的firstTask任務,之後便一直從workQueue阻塞佇列中獲取任務並執行,如果你想在任務執行前後做點啥不可告人的小動作,你可以實現ThreadPoolExecutor以下兩個方法:

protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Runnable r, Throwable t) { }
複製程式碼

這樣一來,我們就可以對任務的執行進行實時監控了。

這裡還需要注意,在finally塊中,將task置為空,目的是為了讓執行緒自行呼叫getTask()方法從workQueue阻塞佇列中獲取任務。

如何保證核心執行緒不被銷燬?

我們之前已經知道執行緒池中可維持corePoolSize數量的常駐核心執行緒,那麼它們是如何保證執行完任務而不被執行緒池回收的呢?在前面的章節中你可能已經到從workQueue佇列中會阻塞式地獲取任務,如果沒有獲取任務,那麼就會一直阻塞下去,很聰明,你已經知道答案了,現在我們來看Doug Lea大神是如何實現的。

java.util.concurrent.ThreadPoolExecutor#getTask:

private Runnable getTask() {
  // 超時標記,預設為false,如果呼叫workQueue.poll()方法超時了,會標記為true
  // 這個標記非常之重要,下面會說到
  boolean timedOut = false;

  for (;;) {
    // 獲取ctl變數值
    int c = ctl.get();
    int rs = runStateOf(c);

    // 如果當前狀態大於等於SHUTDOWN,並且workQueue中的任務為空或者狀態大於等於STOP
    // 則操作AQS減少工作執行緒數量,並且返回null,執行緒被回收
    // 也說明假設狀態為SHUTDOWN的情況下,如果workQueue不為空,那麼執行緒池還是可以繼續執行剩下的任務
    if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
      // 操作AQS將執行緒池中的執行緒數量減一
      decrementWorkerCount();
      return null;
    }

    // 獲取執行緒池中的有效執行緒數量
    int wc = workerCountOf(c);

    // 如果開發者主動開啟allowCoreThreadTimeOut並且獲取當前工作執行緒大於corePoolSize,那麼該執行緒是可以被超時回收的
    // allowCoreThreadTimeOut預設為false,即預設不允許核心執行緒超時回收
    // 這裡也說明了在核心執行緒以外的執行緒都為“臨時”執行緒,隨時會被執行緒池回收
    boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
    
    // 這裡說明了兩點銷燬執行緒的條件:
    // 1.原則上執行緒池數量不可能大於maximumPoolSize,但可能會出現併發時操作了setMaximumPoolSize方法,如果此時將最大執行緒數量調少了,很可能會出現當前工作執行緒大於最大執行緒的情況,這時就需要執行緒超時回收,以維持執行緒池最大執行緒小於maximumPoolSize,
    // 2.timed && timedOut 如果為true,表示當前操作需要進行超時控制,這裡的timedOut為true,說明該執行緒已經從workQueue.poll()方法超時了
    // 以上兩點滿足其一,都可以觸發執行緒超時回收
    if ((wc > maximumPoolSize || (timed && timedOut))
        && (wc > 1 || workQueue.isEmpty())) {
      // 嘗試用AQS將執行緒池執行緒數量減一
      if (compareAndDecrementWorkerCount(c))
        // 減一成功後返回null,執行緒被回收
        return null;
      // 否則迴圈重試
      continue;
    }

    try {
      // 如果timed為true,阻塞超時獲取任務,否則阻塞獲取任務
      Runnable r = timed ?
        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
      workQueue.take();
      if (r != null)
        return r;
      // 如果poll超時獲取任務超時了, 將timeOut設定為true
      // 繼續迴圈執行,如果碰巧開發者開啟了allowCoreThreadTimeOut,那麼該執行緒就滿足超時回收了
      timedOut = true;
    } catch (InterruptedException retry) {
      timedOut = false;
    }
  }
}
複製程式碼

我把我對getTask()方法原始碼的深度解析寫在原始碼對應的地方了,該方法就是實現預設的情況下核心執行緒不被銷燬的核心實現,其實現思路大致是:

  1. 將timedOut超時標記預設設定為false;
  2. 計算timed的值,該值決定了執行緒的生死大權,(timed && timedOut) 即是執行緒超時回收的條件之一,需要注意的是第一次(timed && timedOut) 為false,因為timedOut預設值為false,此時還沒到poll超時獲取的操作;
  3. 根據timed值來決定是用阻塞超時獲取任務還是阻塞獲取任務,如果用阻塞超時獲取任務,超時後timedOut會被設定為true,接著繼續迴圈,此時(timed && timedOut) 為true,滿足執行緒超時回收。

嘔心瀝血的一篇原始碼解讀到此結束,希望能助同學們徹底吃透執行緒池的底層原理,以後遇到面試官問你執行緒池的問題,你就說看過科代表的執行緒池原始碼解讀,面試官這時就會誇你:

這同學基礎真紮實!

從原始碼的角度解析執行緒池執行原理

微信公眾號「Java科代表」

相關文章