深入學習執行緒池原理

木瓜芒果發表於2019-05-20

   在前面的文章:<<執行緒池原理初探>>中我們學習了執行緒池的基本用法和優點,並且從原始碼層面學習了執行緒池的內部資料結構以及執行狀態表徵方法,這是最基礎但是又很重要的一環,有了這一步鋪墊我們便可以開始進一步的原始碼學習之旅了。

  本文會從如下幾個方面展開:

  工作執行緒--Worker

  如何提交任務

  如何新增Worker

  worker是如何開始工作的

  執行緒池如何結束Worker的工作

  終止執行緒池原理

  總結

 

1. 工作執行緒--Worker

  上文說到,執行緒池中的工作執行緒是儲存在一個hashSet中,這樣說其實並不是很準確,因為執行緒池中執行任務的基本單元是一個定義在ThreadPoolExecutor中的內部類Worker,繼承自AQS,並實現了Runnable介面,其本身就是一個任務,內部封裝了驅動其執行的執行緒,而這個worker才是儲存在hashSet中的。我們來看一下:

    private final class Worker extends AbstractQueuedSynchronizer implements Runnable
    {
        /** 這才是真的執行緒,worker只是一個runnable,需要執行緒來驅動,而這個執行緒則是封裝在worker中,worker在其自己的run()方法中再去執行佇列中的任務 */
        final Thread thread;
        /** 第一個要執行的任務 */
        Runnable firstTask;
        /** 已完成的任務數量 */
        volatile long completedTasks;

     // 建構函式 Worker(Runnable firstTask) { setState(
-1); // 防止在runWorker之前被中斷 this.firstTask = firstTask; this.thread = getThreadFactory().newThread(this); } /** 委託給ThreadPoolExecutor中的runWorker方法 */ public void run() { runWorker(this); } protected boolean isHeldExclusively() { return getState() != 0; } protected boolean tryAcquire(int unused) { if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } protected boolean tryRelease(int unused) { setExclusiveOwnerThread(null); setState(0); return true; } public void lock() { acquire(1); } public boolean tryLock() { return tryAcquire(1); } public void unlock() { release(1); } public boolean isLocked() { return isHeldExclusively(); } void interruptIfStarted() { Thread t; if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) { try { t.interrupt(); } catch (SecurityException ignore) { } } } }
  • 在Worker中封裝了一個Thread,這個才是真正的執行緒池幫我們管理的執行緒。Worker只是一個runnable,需要執行緒來驅動,而這個執行緒又是封裝在Worker內部,Worker在其自己的run()方法中再去執行佇列中的任務;
  • 在Worker中封裝的Thread會在Worker的初始化方法中進行賦值,通過執行緒池內部的ThreadFactory獲取一個Thread例項,也就是說所有執行緒池中的執行緒都是通過ThreadFactory這一工廠產生的;
  • Worker繼承自AQS,實現了一個簡單的不可重入互斥鎖;
  • 為了防止線上程實際開始執行任務之前被中斷,在Worker的初始化方法中直接將鎖狀態變數state置為-1,在runWorker方法中會將其清除為0;

  這裡其實只要重點關注Worker自身是一個任務,它將執行緒封裝起來了,由該執行緒來驅動Worker的run()方法,然後Worker在其自己的run()方法中不斷地從任務佇列中獲取任務並執行。

  關於Worker,我們先了解這麼多就夠了,一些更深入的細節還需要結合ThreadPoolExecutor自身的邏輯來理解才更容易弄清楚。

 

2. 如何提交任務

   其實ThreadPoolExecutor的execut()方法是一個很好的看原始碼的入口,因為這也許是我們使用的最多的方法,並且執行緒池的主要邏輯也在這個方法中。該方法對於使用者來說就是向執行緒池提交任務,至於提交任務之後的邏輯,是否要新建執行緒、是否將任務加入阻塞佇列中、是否要拒絕任務等等,這些對使用者都是透明的,這也是我們接下來要重點探索的:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    // 檢查工作執行緒的數量,低於corePoolsize則新增Worker
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // isRunning()用來檢查執行緒池是否處於執行狀態
    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);
    }
    // 到這裡已經意味著已經飽和或者被shutdown了,嘗試新增一個非核心worker,如果失敗就就直接執行拒絕
    else if (!addWorker(command, false))
        reject(command);
}

  如上,配合註釋更容易理解,總結一下,一共分成3步:

  • 檢查工作執行緒的數量,低於corePoolsize則新增Worker;
  • 判斷執行緒池是否處於執行狀態,如果在,判斷任務佇列是否允許插入,插入成功再次驗證執行緒池是否處於執行狀態,如果不在執行狀態則移除插入的任務,然後丟擲拒絕策略,否則檢查存活執行緒的數量,如果沒有執行緒了,就新增一個Worker;
  • 如果執行到這一步意味著執行緒池已經飽和或者被shutdown了,嘗試新增一個非核心worker,如果失敗就就直接執行拒絕;

 

3. 如何新增Worker

  接下來我們再來看一下如何新增Worker,這部分邏輯是在addWorker()方法中,這部分主要負責建立新的執行緒並執行任務:

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
     // 如果執行緒池的狀態值大於或等SHUTDOWN,則不處理提交的任務,直接返回
        if (rs >= SHUTDOWN &&
            !(rs == SHUTDOWN &&
               firstTask == null &&
               !workQueue.isEmpty()))
            return false;
        
        for (;;) {
            int wc = workerCountOf(c);
            // 如果當前執行緒數量太多則直接退出
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // 做自旋,如果當前執行緒數量更新成功則跳出retry執行後面addworker邏輯
            if (compareAndIncrementWorkerCount(c))
                break retry;
            // 重新讀取ctl,如果執行緒池狀態改變,則從retry重新執行
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            // 獲取執行緒池主鎖
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                int rs = runStateOf(ctl.get());
                // 新增執行緒到workers中(執行緒池中)
                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) {
                // 啟動新建的執行緒,此時新增的worker會被驅動執行其run()方法
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

  主要分為如下5步:

  • 判斷是否需要新增worker:
    • 如果執行緒池的狀態值大於SHUTDOWN,則不需新增worker,直接返回false;    
    • 如果執行緒池的狀態值等於SHUTDOWN,此時如果傳入的firstTask不為空,則不需要新增worker,則直接返回false;  
    • 如果執行緒池的狀態值等於SHUTDOWN,且傳入的firstTask為空,則檢查workQueue是否為空,是則不需要新增worker,直接返回false;  
    • 否則代表判斷通過,繼續執行後面邏輯;  
  • 做自旋,更新建立執行緒數量;
    • 如果此時執行緒數量太多(超過ctl能儲存的數量或者超過指定的執行緒池最大執行緒數量),則直接返回false;  
    • 利用cas更新執行緒數量(ctl加1),成功則跳出自旋繼續後面的操作;  
    • 如果更新失敗則檢查執行緒執行狀態,如果發生改變則重新開始addWorker,否則繼續自旋更新ctl;  
  • 獲取執行緒池主鎖,通過ReentrantLock鎖保證執行緒安全,因為workers這個hashSet對於使用者來說相當於共享變數,所以這裡要加鎖;
  • 新增新執行緒到workers中(一個HashSet),釋放鎖;
  • 如果新增成功則啟動新建的執行緒;
  • 如果執行緒啟動失敗,代表新增也失敗了,則執行回退補償邏輯,在addWorkerFailed()方法中;

 

4. Worker是如何工作的

  在addWorker中新增了新的worker之後會啟動其封裝的執行緒,該worker也會隨之被執行緒驅動執行(因為worker繼承自Runnable)。前面講Worker的時候我們知道其run()方法中只呼叫了一個方法,就是定義在ThreadPoolExecutor中的runWorker(),這裡才是執行worker的主要工作邏輯:

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
   // 因為Worker自身就是一把簡單的不可重入互斥鎖(聽起來好像也不簡單。。),這裡呼叫unlock()是為了將state的狀態從-1改為0 w.unlock();
// allow interrupts boolean completedAbruptly = true; try { // 如果是第一次執行任務,或者從佇列中能夠獲取到任務,則執行 while (task != null || (task = getTask()) != null) { w.lock(); // If pool is stopping, ensure thread is interrupted; // if not, ensure thread is not interrupted. This // requires a recheck in second case to deal with // shutdownNow race while clearing interrupt // 根據執行緒池的狀態來判斷是否需要將當前執行緒interrupt 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 = null; // task置空,以便while迴圈中獲取新任務 w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); } }

   這是一個被final修飾的方法,不能被重寫。總結一下其邏輯(比較複雜):

  • 根據執行緒池的狀態來判斷是否需要將當前執行緒interrupt,如果執行緒池為stop且當前執行緒未被interrupt,則interrupt當前執行緒;反之如果執行緒池為running或shutdown,則需要確保當前執行緒未被interrupt(原始碼裡是通過Thread.interrupted()來保證的,因為其可以將中斷狀態清零),並且再次檢查執行緒池狀態是否為stop,如果是則邏輯同上;
  • 如果是第一次執行任務,或者從佇列中能夠獲取到任務,則繼續迴圈執行;
  • 獲取鎖;
  • 執行任務開始前的鉤子函式;
  • 呼叫task的run方法,真正開始執行任務;
  • 執行任務後鉤子函式;    
  • 將task置空,釋放鎖,完成任務+1;
  • 執行退出worker邏輯,需要將worker從workers中移除;

  好了, 看了不少原始碼,我們稍微停一下:

  • 使用者呼叫執行緒池的execute()方法之後,執行緒池根據情況有三種操作:addWorker、將任務放到阻塞佇列中、拒絕;
  • 後面兩種操作很簡單,addWorker操作會步驟多一些,主要包括:做一些必要的判斷、建立新的Worker並將其加入到workers中、將worker跑起來;
  • worker跑起來之後會不斷地從阻塞佇列中取任務並執行;

  上面的runWorker()方法中我們也看到了,worker跑起來之後取就進入了一個while迴圈中,不斷地取任務並執行,好像沒有看到哪裡可以退出,那執行緒池又是如何讓worker停下來的呢?我們接著往下看。

 

5. 執行緒池如何結束Worker的工作

   在上面那節的程式碼中我們可以看到Worker啟動之後,一直在一個while()迴圈中工作,如果退出了這個迴圈,run()方法也就鄰近結束了。所以只要能夠讓執行中的worker退出自己的while()迴圈就能結束worker了,那我們就要來看一下while迴圈中的條件:

while (task != null || (task = getTask()) != null) {
    。。。  
}

  有兩個條件:

  • task不為空,當work執行了一個任務之後,這個就會被置空,所以第一個條間很多情況下都是false;
  • 從getTask()獲取任務,如果返回為空,那麼迴圈條件為false,迴圈退出;

  這就是執行緒池關閉執行緒的開關入口,我們來看一下這個getTast()方法吧:

  private Runnable getTask() {
      boolean timedOut = false; // Did the last poll() time out?

      for (;;) {
          int c = ctl.get();
          int rs = runStateOf(c);

          // 只在必要的時候才檢查任務佇列是否為空
          if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
              decrementWorkerCount();
              return null;
          }

          int wc = workerCountOf(c);

          // Are workers subject to culling?
          boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

          if ((wc > maximumPoolSize || (timed && timedOut))
              && (wc > 1 || workQueue.isEmpty())) {
              if (compareAndDecrementWorkerCount(c))
                  return null;
              continue;
          }

          try {
              Runnable r = timed ?
                  workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                  workQueue.take();
              if (r != null)
                  return r;
              timedOut = true;
          } catch (InterruptedException retry) {
              timedOut = false;
          }
      }
  }
  • 首先要判斷執行狀態是否大於等於SHUTDOWN(除了RUNNING之外的狀態);
  • 如果是,並且滿足如下兩個條件之一則執行decrementWorkerCount()並返回null,認為這個執行緒是多餘的,需要刪除:
    • 執行緒池執行狀態為SHUTDOW,且任務佇列為空;  
    • 執行緒池執行狀態大於等於STOP;  
  • 否則繼續執行;
  • 如果同時滿足如下條件,則利用CAS機制嘗試減少執行執行緒數,成功則返回空,失敗則跳到第1步:

    • 要麼是執行執行緒數量大於最大執行緒數量maximumPoolSize,要麼是存在非核心執行緒或者允許核心執行緒超時銷燬並且已超時;  

    • 執行執行緒數大於1或任務佇列為空;

  • 接著就要從任務佇列取任務了:

    • 如果允許超時則呼叫poll取任務,這個方法會使當前執行緒阻塞一段指定時間;  

    • 否則呼叫take()取任務,這個方法會使當前執行緒一直阻塞,直到獲取到任務或者被當前執行緒被中斷;  

  • 如果取出任務則返回,沒有的話則將timedOut置為true,標記為已超時(代表核心執行緒等待時間過長,需要刪除),重新進入到步驟1,繼續迴圈執行;

  這裡的邏輯比較多,因為有涉及到是否允許核心執行緒超時,所以需要細細品味。當呼叫getTask()為拿到任務,就意味著當前執行緒該做的工作已經完成了,不用再迴圈取任務執行了,剩下就是執行processWorkerExit()結束工作了。

6. 終止執行緒池原理

   現在提交任務、執行任務、以及停止任務的入口,這些邏輯我們都看完了,我們來看一下如何停止執行緒池。主要有兩個方法:shutdown、shutdownNow,從名字我們可以看出區別:

  • shutdown()執行之後執行緒池會停止接收任務,但是還是會把任務池中的任務執行完再結束;
  • shutdownNow()執行之後執行緒池不僅會停止接收任務,而且會把任務池中未執行的任務都清空,直接結束;

  我們來看一下具體實現細節:

6.1 shutdown

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
       // 修改執行緒池執行狀態為SHUTDOWN advanceRunState(SHUTDOWN);
       // 中斷空閒執行緒 interruptIdleWorkers();
       // 預留的鉤子函式 onShutdown();
// hook for ScheduledThreadPoolExecutor } finally { mainLock.unlock(); }
     // tryTerminate(); }

  邏輯比較清晰:

  • 首先,獲取執行緒池的主鎖,只有1個執行緒可以操作;
  • 許可權判斷;
  • 利用CAS機制不斷嘗試修改執行緒池狀態為SHUTDOWN,直到成功為止;
  • 中斷空閒執行緒;
  • 釋放鎖;
  • 執行tryTerminate();

  先來看一下如何利用CAS修改執行緒池狀態,如下程式碼是advanceRunState()的實現,可以看到在迴圈中不斷呼叫原子類ctl的compareAndSet()方法來設定值,這就是利用CAS機制:

    private void advanceRunState(int targetState) {
     // 進入迴圈
for (;;) { int c = ctl.get();
       // 如果狀態修改成功則退出迴圈
if (runStateAtLeast(c, targetState) || ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c)))) break; } }

  接下來看一下如何中斷空閒執行緒,也很簡單,就是對所有worker進行遍歷,判斷其是否被中斷,如果沒有則嘗試設定其中斷標誌。這裡只是說了一下基本流程,有些細節沒有提到,需要程式碼中體會:

    private void interruptIdleWorkers() {
        interruptIdleWorkers(false);
    }

    private void interruptIdleWorkers(boolean onlyOne) {
     // 獲取執行緒池的鎖,並上鎖
final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try {
       // 遍歷workers
for (Worker w : workers) { Thread t = w.thread;
          // 判斷worker封裝的Thread例項是否被中斷,如果沒有則嘗試獲取worker自己的鎖
if (!t.isInterrupted() && w.tryLock()) { try {
               // 設定中斷狀態 t.interrupt(); }
catch (SecurityException ignore) { } finally { w.unlock(); } }
          // 根據傳入的引數,只中斷一個worker
if (onlyOne) break; } } finally { mainLock.unlock(); } }

   最後我們再來看一下tryTerminate()的邏輯,配合程式碼看效果會更好,這裡簡單說一下,首先會有幾輪判斷,是否需要執行terminate(),接著會利用CAS機制嘗試修改執行緒池狀態為TIDYING,成功則執行terminate(),失敗則迴圈執行:

    final void tryTerminate() {
        for (;;) {
            int c = ctl.get(); 
       /**
        * 滿足如下兩個條件則直接返回
        * 1. 執行緒池當前狀態為RUNNING、TIDYING、TERMINATED
        * 2. 執行緒池當前狀態為SHUTDOWN且任務佇列不為空,那還要繼續將佇列中的任務執行完才能結束
        **/
if (isRunning(c) || runStateAtLeast(c, TIDYING) || (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty())) return;
       // 到這裡代表執行緒池的狀態為SHUTDOWN或STOP,如果還有存活執行緒,則嘗試中斷一個並返回
if (workerCountOf(c) != 0) { // Eligible to terminate interruptIdleWorkers(ONLY_ONE); return; } final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try {
          // 嘗試將執行緒池狀態修改為TIDYING,修改成功則執行terminated(),如果沒有則繼續迴圈執行
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { try { terminated(); } finally {
               // 執行完terminated()之後需要確保將執行緒池狀態修改為TERMINATED ctl.set(ctlOf(TERMINATED,
0)); termination.signalAll(); } return; } } finally { mainLock.unlock(); } // else retry on failed CAS } }

  其實shutdown()最終還是通過設定工作執行緒的中斷狀態來實現結束中斷執行緒的,關於這種方式我們前面也是專門寫過一篇文章的:<<執行緒間通訊>>。具體是如何結束的,線上程執行的過程中會不斷的呼叫getTask()從任務佇列獲取任務,在getTask()中會對中斷狀態進行監控,一旦發現之後會根據具體邏輯執行對應操作,具體參考getTask()的程式碼。

6.2 shutdownNow

   看完shutdown()的我們再來看一下shutdownNow()的邏輯:

    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP);
            interruptWorkers();
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

  基本流程和shutdown()類似,advanceRunState()和tryTerminate()是一樣的,我們就不再贅述了,重點來看一下interruptWorkers()的邏輯:

    private void interruptWorkers() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers)
                w.interruptIfStarted();
        } finally {
            mainLock.unlock();
        }
    }

  這樣看很簡單,就是遍歷所有worker,呼叫其interruptIfStarted()方法,這個方法實現在Worker中,我們來看一下這個方法,也比較清晰,就是判斷一下再決定是否設定執行緒中斷標誌位,可見,其和shutdown停止執行緒的方式是一樣的,區別主要在於設定執行緒狀態的不同以及將任務佇列中的任務丟棄,即drainQueue()方法:

    void interruptIfStarted() {
        Thread t;
        if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
            try {
                t.interrupt();
            } catch (SecurityException ignore) {
            }
        }
    }

    private List<Runnable> drainQueue() {
        BlockingQueue<Runnable> q = workQueue;
        ArrayList<Runnable> taskList = new ArrayList<Runnable>();
        // 將阻塞佇列中的任務全部移除並新增到taskList中
        q.drainTo(taskList);
        // 再檢查一次佇列是否有任務
        if (!q.isEmpty()) {
            for (Runnable r : q.toArray(new Runnable[0])) {
                if (q.remove(r))
                    taskList.add(r);
            }
        }
        return taskList;
    }

 

7. 總結

  • 執行緒池中通過阻塞佇列來儲存接收使用者提交的任務;
  • 執行緒池的基本工作單元為Worker,為實現在ThreadPoolExecutor中的內部類,繼承了Runnable,它封裝了驅動自己執行的執行緒,工作時worker會不斷從任務佇列中獲取任務並執行;
  • 執行緒池內部通過一個HashSet來儲存worker,這才是真的“池”;
  • 使用者通過呼叫執行緒池的execute()將任務提交給執行緒池,之後由執行緒池來統一分配執行緒執行任務;
  • 使用者可以呼叫shutdown()或shutdownNow()來停止執行緒池;
    • 呼叫shutdown()之後執行緒池不再接收新的任務,但是會將任務佇列中的任務執行完再逐一將執行緒銷燬並停止執行緒池;  
    • 呼叫shutdownNow()之後執行緒池不僅不再接收新的任務,而且會將任務佇列中未執行的任務清空丟棄,並且將待正在執行的執行緒執行完畢就銷燬執行緒,然後停止執行緒池;  

  其實呢,在啃執行緒池原始碼的過程中,還是要費一些心思的,尤其是要弄明白如何新增任務、如何新增Worker、Worker如果工作以及如何停止Worker的工作這一整套流程,中間確實邏輯比較複雜,但是呢在探索的過程中會不斷有新的發現,越啃越細,越啃越清晰。我其實也不是一兩天就看明白了,最早只是大概看了一遍,然後做了一些筆記,隔了幾個月之後再來看,又有新的收穫,所以就有了這篇文章。看到這裡說明你也看懂了,恭喜你在學習的路上又有精進了   ^_^

相關文章