執行緒池 execute() 的工作邏輯

犀利豆發表於2019-01-18

原文地址:www.xilidou.com/2018/02/09/…

最近在看《Java併發程式設計的藝術》回顧執行緒池的原理和引數的時候發現一個問題,如果 corePoolSize = 0 且 阻塞佇列是無界的。執行緒池將如何工作?

我們先回顧一下書裡面描述執行緒池execute()工作的邏輯:

  1. 如果當前執行的執行緒,少於corePoolSize,則建立一個新的執行緒來執行任務。
  2. 如果執行的執行緒等於或多於 corePoolSize,將任務加入 BlockingQueue。
  3. 如果 BlockingQueue 內的任務超過上限,則建立新的執行緒來處理任務。
  4. 如果建立的執行緒數是單錢執行的執行緒超出 maximumPoolSize,任務將被拒絕策略拒絕。

看了這四個步驟,其實描述上是有一個漏洞的。如果核心執行緒數是0,阻塞佇列也是無界的,會怎樣?如果按照上文的邏輯,應該沒有執行緒會被執行,然後執行緒無限的增加到佇列裡面。然後呢?

於是我做了一下試驗看看到底會怎樣?

public class threadTest {
    private final static ThreadPoolExecutor executor = new ThreadPoolExecutor(0,1,0, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger();
        while (true) {
            executor.execute(() -> {
                System.out.println(atomicInteger.getAndAdd(1));
            });
        }
    }
}
複製程式碼

結果裡面的System.out.println(atomicInteger.getAndAdd(1));語句執行了,與上面的描述矛盾了。到底發生了什麼?執行緒池建立執行緒的邏輯是什麼?我們還是從原始碼來看看到底執行緒池的邏輯是什麼?

ctl

要了解執行緒池,我們首先要了解的執行緒池裡面的狀態控制的引數 ctl。

  • 執行緒池的ctl是一個原子的 AtomicInteger。
  • 這個ctl包含兩個引數 :
    • workerCount 啟用的執行緒數
    • runState 當前執行緒池的狀態
  • 它的低29位用於存放當前的執行緒數, 因此一個執行緒池在理論上最大的執行緒數是 536870911; 高 3 位是用於表示當前執行緒池的狀態, 其中高三位的值和狀態對應如下:
    • 111: RUNNING
    • 000: SHUTDOWN
    • 001: STOP
    • 010: TIDYING
    • 110: TERMINATED

為了能夠使用 ctl 執行緒池提供了三個方法:

    // 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; }
複製程式碼

execute

外界通過 execute 這個方法來向執行緒池提交任務。

先看程式碼:


 public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();

        //如果工作執行緒數小於核心執行緒數,
        if (workerCountOf(c) < corePoolSize) {
            //執行addWork,提交為核心執行緒,提交成功return。提交失敗重新獲取ctl
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }

        //如果工作執行緒數大於核心執行緒數,則檢查執行緒池狀態是否是正在執行,且將新執行緒向阻塞佇列提交。
        if (isRunning(c) && workQueue.offer(command)) {

            //recheck 需要再次檢查,主要目的是判斷加入到阻塞隊裡中的執行緒是否可以被執行
            int recheck = ctl.get();

            //如果執行緒池狀態不為running,將任務從阻塞佇列裡面移除,啟用拒絕策略
            if (! isRunning(recheck) && remove(command))
                reject(command);
            // 如果執行緒池的工作執行緒為零,則呼叫addWoker提交任務
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }

        //新增非核心執行緒失敗,拒絕
        else if (!addWorker(command, false))
            reject(command);
    }

複製程式碼

addWorker

    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            //獲取執行緒池狀態
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            // 判斷是否可以新增任務。
            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;
                //CAS 增加工作執行緒數
                if (compareAndIncrementWorkerCount(c))
                    break 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;

        //上面的邏輯是考慮是否能夠新增執行緒,如果可以就cas的增加工作執行緒數量
        //下面正式啟動執行緒
        try {
            //新建worker
            w = new Worker(firstTask);

            //獲取當前執行緒
            final Thread t = w.thread;
            if (t != null) {
                //獲取可重入鎖
                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狀態
                    // 或者執行緒處於SHUTDOWN狀態,且firstTask == null(可能是workQueue中仍有未執行完成的任務,建立沒有初始任務的worker執行緒執行)
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        // 當前執行緒已經啟動,丟擲異常
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        //workers 是一個 HashSet 必須在 lock的情況下操作。
                        workers.add(w);
                        int s = workers.size();
                        //設定 largeestPoolSize 標記workAdded
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                //如果新增成功,啟動執行緒
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            //啟動執行緒失敗,回滾。
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }
複製程式碼

先看看 addWork() 的兩個引數,第一個是需要提交的執行緒 Runnable firstTask,第二個引數是 boolean 型別,表示是否為核心執行緒。

execute() 中有三處呼叫了 addWork() 我們逐一分析。

  • 第一次,條件 if (workerCountOf(c) < corePoolSize) 這個很好理解,工作執行緒數少於核心執行緒數,提交任務。所以 addWorker(command, true)
  • 第二次,如果 workerCountOf(recheck) == 0 如果worker的數量為0,那就 addWorker(null,false)。為什麼這裡是 null ?之前已經把 command 提交到阻塞佇列了 workQueue.offer(command) 。所以提交一個空執行緒,直接從阻塞佇列裡面取就可以了。
  • 第三次,如果執行緒池沒有 RUNNING 或者 offer 阻塞佇列失敗,addWorker(command,false),很好理解,對應的就是,阻塞佇列滿了,將任務提交到,非核心執行緒池。與最大執行緒池比較。

至此,重新歸納execute()的邏輯應該是:

  1. 如果當前執行的執行緒,少於corePoolSize,則建立一個新的執行緒來執行任務。
  2. 如果執行的執行緒等於或多於 corePoolSize,將任務加入 BlockingQueue。
  3. 如果加入 BlockingQueue 成功,需要二次檢查執行緒池的狀態如果執行緒池沒有處於 Running,則從 BlockingQueue 移除任務,啟動拒絕策略。
  4. 如果執行緒池處於 Running狀態,則檢查工作執行緒(worker)是否為0。如果為0,則建立新的執行緒來處理任務。如果啟動執行緒數大於maximumPoolSize,任務將被拒絕策略拒絕。
  5. 如果加入 BlockingQueue 。失敗,則建立新的執行緒來處理任務。
  6. 如果啟動執行緒數大於maximumPoolSize,任務將被拒絕策略拒絕。

總結

回顧我開始提出的問題:

如果 corePoolSize = 0 且 阻塞佇列是無界的。執行緒池將如何工作?

這個問題應該就不難回答了。

最後

《Java併發程式設計的藝術》是一本學習 java 併發程式設計的好書,在這裡推薦給大家。

同時,希望大家在閱讀技術資料的時候要仔細思考,結合原始碼,發現,提出問題,解決問題。這樣的學習才能高效且透徹。

歡迎關注我的微信公眾號

二維碼

相關文章