JAVA併發程式設計:執行緒池ThreadPoolExecutor原始碼分析

﹏半生如夢願夢如真て發表於2020-11-09

  前面的文章已經詳細分析了執行緒池的工作原理及其基本應用,接下來本文將從底層原始碼分析一下執行緒池的執行過程。在看原始碼的時候,首先帶著以下兩個問題去仔細閱讀。一是執行緒池如何保證核心執行緒數不會被銷燬,空閒執行緒數會被銷燬的呢?二是核心執行緒和空閒執行緒的區別到底是什麼?
  首先,我們先來看一下以下兩個示例,從程式碼示例走入底層原始碼,真正做到了如指掌。

1、示例分析

package cn.lspj.threadpool;

import java.text.SimpleDateFormat;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 從示例到底層原始碼分析 ThreadPoolExecutor 的執行過程
 * <p>
 * 1、核心執行緒和空閒執行緒的區別?
 * 2、執行緒池如何保證核心執行緒不會銷燬,空閒執行緒會被銷燬呢?
 * <p>
 * 接下來帶著這兩個問題來仔細分析一下
 */
public class UseThreadPoolExecutor {

    private static AtomicInteger i = new AtomicInteger();
    private final static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

    private static String getSystemTime() {
        return sdf.format(System.currentTimeMillis());
    }


    /**
     * 定義核心執行緒數為1,最大執行緒數為2,空閒執行緒存活3秒,佇列長度為5
     * 主執行緒提交6個任務,因為佇列能夠存放得下提交的所有任務,所以不會啟用空閒執行緒來執行提交的任務,確切的來說這裡永遠只會有一個執行緒在工作。
     * 因為佇列能夠存放得下提交的任務,所以不會啟用空閒執行緒。
     */
    private static void m1() {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,
                2,
                3,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5),
                (r) -> {
                    return new Thread(r, "t" + i.incrementAndGet());
                });

        for (int i = 0; i < 6; i++) {
            int finalI = i;
            threadPoolExecutor.execute(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(getSystemTime() + " 執行緒 " + Thread.currentThread().getName() + " 執行任務 " + finalI);
            });
        }
    }

    /**
     * 一個很有意思的例子,定義核心執行緒數為0,最大執行緒數為10000,空閒執行緒存活5秒,佇列長度為10000
     * 主執行緒提交5個任務,講道理佇列能夠存放得下提交的所有任務,不會啟用空閒執行緒,執行緒池也不會執行任務。
     * <p>
     * 事實真的會如此嗎???那肯定不會,如果不會執行那還扯個蛋呢。
     *
     * 那執行這個任務的執行緒是什麼執行緒呢?並不是核心執行緒(因為核心執行緒數為0),執行緒池會啟動一個空閒執行緒來執行任務。
     */
    private static void m2() {

        /**
         * 核心執行緒和空閒執行緒的區別?
         * 1、空閒執行緒可能會銷燬,空閒時間達到 keepAliveTime,執行緒將被立刻銷燬。
         * 2、核心執行緒不會銷燬,沒有任務執行的時候會一直阻塞,除非呼叫執行緒池的 shutdown() 方法。
         *
         * 執行緒池是如何保證核心執行緒不會被銷燬?空閒執行緒數為什麼會銷燬呢?接下來我們深入分析一下執行緒池原始碼底層實現。
         *
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0,
                10000,
                10,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10000),
                (r) -> {
                    return new Thread(r, "t" + i.incrementAndGet());
                });

        for (int i = 0; i < 5; i++) {
            int finalI = i;
            threadPoolExecutor.execute(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(getSystemTime() + " 執行緒 " + Thread.currentThread().getName() + " 執行任務 " + finalI);
            });
        }
    }

    public static void main(String[] args) {
        //m1();
        m2();
    }
}

方法m1():
  定義核心執行緒數為1,最大執行緒數為2,空閒執行緒存活3秒,佇列長度為5。主執行緒提交6個任務,因為佇列能夠存放得下提交的所有任務,所以不會啟用空閒執行緒來執行提交的任務,確切的來說這裡永遠只會有一個執行緒在工作。 因為佇列能夠存放得下提交的任務,所以不會啟用空閒執行緒。
執行結果如下:
在這裡插入圖片描述
方法m2():
  一個很有意思的例子,定義核心執行緒數為0,最大執行緒數為10000,空閒執行緒存活10秒,佇列長度為10000。主執行緒提交5個任務,講道理佇列能夠存放得下提交的所有任務,不會啟用空閒執行緒,執行緒池也不會執行任務。事實真的會如此嗎???那肯定不會,如果不會執行那還扯個蛋呢。那執行這個任務的執行緒是什麼執行緒呢?並不是核心執行緒(因為核心執行緒數為0),執行緒池會啟動一個空閒執行緒來執行任務。從執行結果來看,任務執行結束10秒後執行緒池就關閉了。

執行結果如下:
在這裡插入圖片描述

2、原始碼分析

  執行緒池是如何保證核心執行緒不會被銷燬?空閒執行緒數為什麼會銷燬的呢?接下來我們深入分析一下執行緒池原始碼底層實現。

2.1 execute方法

public void execute(Runnable command) {
    // 判斷command任務是否為空
    if (command == null)
        throw new NullPointerException();

    // 前面的文章已經講解過ctl此變數的含義了,高3位表示執行緒池的狀態,低29位表示執行緒池的工作執行緒數
    int c = ctl.get();
    // 判斷執行緒池中當前工作執行緒數是否小於核心執行緒數
    if (workerCountOf(c) < corePoolSize) {
        // 如果執行緒池中當前工作執行緒數是否小於核心執行緒數,則執行addWorker()方法建立新執行緒執行command任務,注意第二個引數為true
        if (addWorker(command, true))
            return; // 如果addWorker()成功則直接返回,結束execute
        // 如果addWorker()失敗,則再次獲取c的值
        c = ctl.get();
    }

    // 程式碼執行到這此處,說明任務要麼放到阻塞佇列中,要麼啟用一個空閒執行緒來執行任務或者執行拒絕策略

    // 如果執行緒池處於RUNNING狀態,把提交的任務成功放入阻塞佇列中
    if (isRunning(c) && workQueue.offer(command)) {
        // 任務加入阻塞佇列成功,recheck 雙重檢查執行緒池狀態
        int recheck = ctl.get();
        // 如果此時執行緒池已經處於非 RUNNING狀態,則將任務remove掉
        if (! isRunning(recheck) && remove(command))
            // 成功從阻塞佇列中remove掉任務,執行拒絕策略
            reject(command);
            //執行緒池處於RUNNING狀態,但是工作執行緒數為0,則建立新執行緒
            //示例中m2方法會執行到此行程式碼
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false); // 注意第二個引數為false
    }
    // 如果不能將任務放入到阻塞佇列中,那麼我們嘗試新增一個新執行緒(此處是空閒執行緒),如果建立執行緒失敗,則執行拒絕策略
    else if (!addWorker(command, false)) // 注意第二個引數為false
        reject(command);
}

  為什麼需要雙重檢查執行緒池的狀態呢?
  因為在多執行緒環境下,執行緒池的狀態可能會隨時變化,ctl.get()方法不是原子操作,很有可能剛獲取執行緒池狀態後執行緒池的狀態就被改變了,因此再次檢查執行緒池狀態是否跟加入到阻塞佇列workQueue之前一致。如果沒有雙重檢查執行緒池的狀態,在多執行緒環境下萬一執行緒池的狀態為非RUNNING狀態,那麼任務永遠不會被執行。

2.2 addWorker方法

  addWorker方法一上來就來兩個for (;; )死迴圈,是不是很懵逼,彆著急,接下來我會帶大家一起詳細分析以下方法的具體實現邏輯,其實也就那麼回事。

private boolean addWorker(Runnable firstTask, boolean core) {
        retry: // 標籤retry:,break 和 continue 的語法,用於跳出多重迴圈,後續會單獨寫一篇文章來分析這個標籤的含義,大家拭目以待吧。。。
        for (;;) {
            // 變數c不在贅述
            int c = ctl.get();
            // 獲取執行緒池的執行狀態
            int rs = runStateOf(c);

            // 下面這個if判斷看起來相當的繞啊
            // 第一個判斷條件,如果rs >= SHUTDOWN成功則執行後面的邏輯運算,否則不會返回false,也就是說如果執行緒池處於執行態,則可以新增任務,執行後續的for (;;)迴圈
            // 第一個判斷條件返回true,此時執行緒池處於非執行狀態,具體看後面的判斷
            // 如果 rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty() 返回false,則return,不會新增任務
            // 具體不再詳細分析了,相信大家都能看懂,主要是圍繞執行緒池的狀態、任務及佇列是否為空來判斷是否需要新增新任務

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;



            // 此處也是使用了一個死迴圈
            for (;;) {
                // wc 變數,表示執行緒池中正在執行的執行緒個數
                int wc = workerCountOf(c);

                // 第一個判斷 wc >= CAPACITY,執行的執行緒數大於等於執行緒池的最大容納個,則return false。
                // 如果wc < CAPACITY,進入第二個判斷,根據傳入的core值判斷wc是與核心執行緒數corePoolSize或者是最大執行緒數maximumPoolSize比較
                // 如果core為true 且 wc >=corePoolSize,則return false
                // 如果core為false 且 wc >=maximumPoolSize,則return false
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                // 使用CAS操作,給工作執行緒數加1,如果成功,則跳出外層死迴圈
                if (compareAndIncrementWorkerCount(c))
                    break retry; // 跳出外層死迴圈
                // 使用CAS操作,給工作執行緒數加1,如果失敗,有可能是其他執行緒做了自增操作。需要繼續更新變數c的值,並繼續執行內層死迴圈做ctl自增操作。
                c = ctl.get();  // Re-read ctl
                // 在繼續執行內層死迴圈之前,需要判斷執行緒池的狀態是否發生了變化
                if (runStateOf(c) != rs)
                    continue retry; // 如果執行緒池狀態從RUNNING變成了STOP,則不能繼續執行內層死迴圈,而是執行外層死迴圈,重新做狀態判斷
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        // 如果外層死迴圈執行結束,並且沒有return false,那說明ctl自增操作成功,接下來的邏輯就是要開始新增新任務了
    boolean workerStarted = false; // 表示執行緒是否啟動
    boolean workerAdded = false; // 表示執行緒是否新增到執行緒池中
    Worker w = null;
    try {
        // 變數w中包括任務、執行緒等等
        w = new Worker(firstTask);
        // 獲取執行緒t
        final Thread t = w.thread;
        // 判斷執行緒t是否為空
        if (t != null) {
            // 如果執行緒t不為null,獲取鎖lock
            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());

                // 如果執行緒池處於執行狀態或者關閉狀態且提交的任務為空,則新增執行緒
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    // 判斷執行緒是否一塊啟動,啟動則丟擲異常IllegalThreadStateException
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    // 將w新增到wokers中,set集合
                    workers.add(w);

                    int s = workers.size();
                    // 判斷是否超過了最大執行緒數,largestPoolSize表示記錄出現過的最大執行緒數
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    // 已經建立了訊息稱並新增到執行緒池中了
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                // 啟動執行緒
                t.start();
                // 執行緒已經啟動成功了
                workerStarted = true;
            }
        }
    } finally {
        // 如果執行緒沒有啟動成功,執行addWorkerFailed方法回滾操作
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

2.3 run方法

  Worker例項新增成功之後,執行緒池把Worker當作執行緒來處理,Worker實現了Runnable介面,接下來我們繼續分析一下Worker中的run()方法。

public void run() {
    runWorker(this);
}
final void runWorker(Worker w) {
    // 獲取當前執行緒
    Thread wt = Thread.currentThread();
    // 獲取提交的任務
    Runnable task = w.firstTask;
    w.firstTask = null; // 設定Worker中的fistTask為空
    // 解鎖,這行程式碼與Worker中的建構函式的setState(-1)對應
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        // 當提交的任務不為空,則處理提交的任務;或者當提交的任務為空,則通過getTask()方法從阻塞隊裡中獲取任務
        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 {
                    // 真正執行的run方法
                    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);
    }
}

2.4 getTask方法

  接下來分析一下getTask()方法,從阻塞佇列BlockingQueue中獲取任務,呼叫佇列的take()方法阻塞式獲取任務或者poll(long timeout, TimeUnit unit)方法超時獲取任務。

private Runnable getTask() {
    // 定義變數timeOut,上一次poll()是否超時
    boolean timedOut = false; // Did the last poll() time out?

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

        // Check if queue empty only if necessary.
        // 如果執行緒池狀態處於非執行狀態,且rs >= STOP 或者佇列為空,則工作執行緒數減1,返回空任務
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        // 獲取執行緒池的工作執行緒數
        int wc = workerCountOf(c);

        // Are workers subject to culling?
        // 定義變數 timed ,是否允許超時獲取任務;
        // 1、allowCoreThreadTimeOut表示是否允許核心執行緒數超時獲取任務,預設false。
        // 2、工作執行緒數是否大於核心執行緒數
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        // 如果工作執行緒數大於最大執行緒數 或者 timed && timedOut 為true(timed為true也就是超時獲取任務,timedOut為true表示上一次已經poll超時).
        // 且工作執行緒數大於1或者阻塞佇列為空,那麼執行compareAndDecrementWorkerCount(c)方法
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c)) // 工作執行緒數減1成功,return null
                return null;
            continue;
        }

        try {
            // 執行緒池如何保證核心執行緒不會銷燬,空閒執行緒會被銷燬呢?原始碼就在此,仔細品味吧
            // 如果timed為true,則使用poll方法超時獲取任務;如果timed為false,則使用take方法阻塞式獲取任務
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null) // 如果獲取的任務不為空,則返回任務r
                return r;
            timedOut = true; //poll 超時,進入下一次死迴圈
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

  原始碼分析就到此為止了,其實還有一些方法沒有去分析,相信讀者有了上面的原始碼瞭解,其他的方法應該都能看明白。通過對以上原始碼的瞭解,回過頭來再看一下文章一開始丟擲來的兩個問題是不是已經得到解決。

一是執行緒池如何保證核心執行緒數不會被銷燬,空閒執行緒數會被銷燬的呢?
  1)、核心執行緒在獲取任務的時候呼叫的是take()方法阻塞式獲取
  2)、空閒執行緒在獲取任務的時候呼叫的是poll(long timeout, TimeUnit unit)方法超時獲取
  因此,執行緒池的核心執行緒數不會被銷燬,空閒執行緒數會被銷燬。

二是核心執行緒和空閒執行緒的區別到底是什麼?
  1)、空閒執行緒可能會銷燬,空閒時間達到 keepAliveTime,執行緒將被立刻銷燬。
  2)、核心執行緒不會銷燬,沒有任務執行的時候會一直阻塞,除非呼叫執行緒池的 shutdown() 或者shutdownNow()方法。


備註:博主微信公眾號,不定期更新文章,歡迎掃碼關注。
在這裡插入圖片描述

相關文章