併發程式設計之 原始碼剖析 執行緒池 實現原理

莫那·魯道發表於2019-01-24

併發程式設計之 原始碼剖析 執行緒池 實現原理

前言

在上一篇文章中我們介紹了執行緒池的使用,那麼現在我們有個疑問:執行緒池到底是怎麼實現的?畢竟好奇是人類的天性。那我們今天就來看看吧,扒開 他的原始碼,一探究竟。

1. 從 Demo 入手

併發程式設計之 原始碼剖析 執行緒池 實現原理

上圖是個最簡單的demo,我們從這個 demo 開始看原始碼,首先一步一步來看。

首先我們手動建立了執行緒池,使用了有數量限制的阻塞佇列,使用了執行緒池工廠提供的預設執行緒工廠,和一個預設的拒絕策略,我們看看預設的執行緒工廠是如何建立的?

併發程式設計之 原始碼剖析 執行緒池 實現原理

併發程式設計之 原始碼剖析 執行緒池 實現原理

預設的執行緒工廠從當前執行緒中獲取執行緒組,設定了預設的執行緒名字字首 pool-xxx-thread-xxx,強制設定為非守護執行緒,強制設定為預設優先順序。

然後我們看看ThreadPoolExecutor 的構造方法:

併發程式設計之 原始碼剖析 執行緒池 實現原理

沒有什麼特殊的東西,主要是一些判斷。

好了,那麼我們看看 execute 方法是如何實現的。

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    // c = -536870911
    int c = ctl.get();
    //  工作執行緒數量小於核心執行緒池設定數,則建立執行緒。
    if (workerCountOf(c) < corePoolSize) {
        // 如果新增成功則直接返回
        if (addWorker(command, true))
            return;
        // 否則再次獲取活動執行緒數量
        c = ctl.get();
    }
    // 如果執行緒池正在執行,並且新增進佇列成功
    if (isRunning(c) && workQueue.offer(command)) {
        // 再次對執行緒池狀態檢查, 因為上面 addWorker 過了並且失敗了,所以需要檢查
        int recheck = ctl.get();
        // 如果狀態不是執行狀態,且從佇列刪除該任務成功並嘗試停止執行緒池
        if (! isRunning(recheck) && remove(command))
            // 拒絕任務
            reject(command);
        // 如果當前工作執行緒數量為0(執行緒池已關閉),則新增一個 null 到佇列中
        else if (workerCountOf(recheck) == 0)
            // 新增個空的任務
            addWorker(null, false);
    }
    // 如果新增佇列失敗,則建立一個任務執行緒,如果失敗,則拒絕
    else if (!addWorker(command, false))
        // 拒絕
        reject(command);
    }
}
複製程式碼

首先,空判斷。

然後判斷,如果正在工作的執行緒小於設定的核心執行緒,則建立執行緒並返回,如果正在工作的執行緒數量大於等於核心執行緒數量,則試圖將任務放入佇列,如果失敗,則嘗試建立一個 maximumPoolSize 的任務。注意,在remove 方法中,該方法已經試圖停止執行緒池的執行。

從這段程式碼中,可以看到,最重要的方法就是 addWorker 和 workQueue.offer(command) 這段程式碼,一個是建立執行緒,一個是放入佇列。後者就是將任務新增到阻塞佇列中。

那麼我們就看看 addWorker 方法。

2. addWorker 方法-----建立執行緒池

private boolean addWorker(Runnable firstTask, boolean core)

該方法很長,樓主說一下這個方法的兩個引數,第一個引數為 Runnable 型別,表示執行緒池中某個執行緒的第一個任務,第二個引數是如果是 true,則建立 core 核心執行緒,如果是 false ,則建立 maximumPoolSize 執行緒。這兩個執行緒的生命週期是不同的。

樓主擷取該方法中最終的程式碼:

併發程式設計之 原始碼剖析 執行緒池 實現原理

其中,在該方法中,建立一個 Worker 物件,該物件代理了任務物件,我們看看該類的構造方法:

併發程式設計之 原始碼剖析 執行緒池 實現原理

通過執行緒工廠建立執行緒,注意,傳遞的是 this ,因此,在上面的程式碼中國,呼叫了 worker 物件的 thread 屬性的 start 方法,實際上就是呼叫了該類的 run 方法。那麼改類的 run 方法是怎麼實現的呢?

併發程式設計之 原始碼剖析 執行緒池 實現原理

呼叫了自身的 runWorker 方法。這個方法非常的重要。

3. Worker.runWorker(Worker w) 方法-------執行緒池的最核心方法


    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        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
                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;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }
複製程式碼

首先說該方法的主要邏輯:

  1. 首先執行 firstTask 的 run 方法。
  2. 然後迴圈獲取阻塞佇列中的任務,並呼叫他們的 run 方法。
  3. 如果執行緒池中的任務異常,就丟擲異常並停止執行執行緒池。

這個方法可以說就是執行緒池的核心,在最開始的設定的核心任務數都是直接呼叫 start 方法啟動執行緒的,啟動之後,這個執行緒並不關閉,而是一直在阻塞佇列上等待,如果有任務就執行任務的run 方法,而不是 start 方法,這點很重要。

而該方法中有幾個注意的地方就是執行緒池留給我們擴充套件的,在執行任務之前,會執行 beforeExecute 方法,該方法預設為空,我們可以實現該方法,在任務執行結束後,在 finally 塊中有 afterExecute 方法,同樣也是空的,我們可以擴充套件。

樓主看到這裡的程式碼後,大為讚歎,Doug Lea 可以說神一般的人物。

那麼,執行緒池還有一個方法, submit 是如何實現的呢?其實核心邏輯也是 runWorker 方法,不然樓主也不會說這個方法是執行緒池的核心。

那我們看看 submit 方法是如何實現的。

4. submit 方法實現原理。

併發程式設計之 原始碼剖析 執行緒池 實現原理

該方法最終也是走 execute 方法的,因此邏輯基本相同,不同的是什麼呢?我們看看。我們看到,第二行程式碼建立了 一個 RunnableFuture 物件,RunnableFuture 是一個介面,具體的實現是什麼呢?我們看看:

FutureTask

FutureTask 物件,該物件也是一個執行緒物件:

併發程式設計之 原始碼剖析 執行緒池 實現原理

那我們就看看該方法的 run 方法。

併發程式設計之 原始碼剖析 執行緒池 實現原理

該方法核心邏輯樓主已經框起來了,其中呼叫了 call 方法,返回一個返回值,並在set 方法中,將返回值設定在一個變數中,如果是異常,則將異常設定在變數中。我們看看set方法:

併發程式設計之 原始碼剖析 執行緒池 實現原理

該方法通過CAS將任務狀態狀態從new變成 COMPLETING,然後,設定 outcome 變數,也就是返回值。最後,呼叫 finishCompletion 方法,完成一些變數的清理工作。

那麼,如果從submit 中獲得返回值呢?這要看get方法:

併發程式設計之 原始碼剖析 執行緒池 實現原理

該方法會判斷狀態,如果狀態還沒有完成,那麼就呼叫 awaitDone 方法等待,如果完成了,呼叫 report 返回值結果。

併發程式設計之 原始碼剖析 執行緒池 實現原理

看見了剛剛設定的 outcome 變數,如果狀態正常,則直接返回,如果狀態為取消,則丟擲異常,其餘情況也丟擲異常。

我們回到 awaitDone 方法,看看該方法如何等待的。

併發程式設計之 原始碼剖析 執行緒池 實現原理

該方法有一個死迴圈,直到有一個確定的狀態返回,如果狀態大於 COMPLETING ,也就是 成功了,就返回該狀態,如果正在進行中,則讓出CPU時間片進行等待。如果都不是,則讓該執行緒阻塞等待。在哪裡喚醒呢?在 finishCompletion 方法中會喚醒該執行緒。

併發程式設計之 原始碼剖析 執行緒池 實現原理

該方法迴圈了等待執行緒連結串列的連結串列,並喚醒連結串列中的每個執行緒。

還有一個需要的注意的地方就是,在任務執行完畢會執行 done 方法,JDK 預設是空的,我們可以擴充套件該方法。比如 Spring 的併發包 org.springframework.util.concurrent 就有2個類重寫了該方法。

5. 總結

好了,到這裡,執行緒池的基本實現原理我們知道了,也解開了樓主一直以來的疑惑,可以說,執行緒池的核心方法就是 runWorker 方法 配合 阻塞佇列,當執行緒啟動後,就從佇列中取出佇列中的任務,執行任務的 run 方法。可以說設計的非常巧妙。而回撥執行緒 callback 也是通過該方法,JDK 封裝了 FutureTask 類來執行他們的 call 方法。

good luck!!!!

相關文章