多執行緒知識梳理(6) 執行緒池四部曲之 ThreadPoolExecutor

澤毛發表於2017-12-21

一、ThreadPoolExecutor 簡介

1.1 優點

多執行緒知識梳理(5) - 執行緒池四部曲之 Executor 框架 中,我們對Executor框架以及它的排程模型進行了簡要的介紹,其中用於對執行緒進行排程和管理的執行緒池是整個框架的核心,通過執行緒池我們可以:

  • 重複利用已經建立的執行緒降低執行緒建立和銷燬造成的消耗。
  • 當任務到達時,任務可以不需要等到執行緒建立就能夠立即執行,提高響應速度。
  • 利用執行緒池對執行緒進行統一分配、調優和監控,提供執行緒的可管理性。

1.2 處理流程

JDK包中,ThreadPoolExecutor就是執行緒池的具體實現,在閱讀原始碼之前,我們先對它的處理流程進行簡要介紹,當我們通過execute/submit方法提交一個任務到執行緒池後,會經過以下的處理流程:

多執行緒知識梳理(6)   執行緒池四部曲之 ThreadPoolExecutor

  1. 如果當前執行的執行緒小於 corePoolSize,則建立新執行緒來執行任務。
  2. 如果執行的執行緒等於或多於 corePoolSize,則將任務加入到等待佇列中。
  3. 如果無法將任務加入到等待佇列,則繼續建立新的執行緒來執行任務。
  4. 如果建立新執行緒使得當前執行的執行緒超過maximumPoolSize,任務將被拒絕。

以上就是ThreadPoolExecutor對於任務的處理流程,其中有幾點需要說明:

  • 當建立一個新執行緒來執行任務時,需要獲取全域性鎖,而如果僅僅是將任務加入到等待佇列中則不需要,
  • 當新執行緒執行完建立它時所指派的第一個任務之後,並不會馬上退出,它會反覆從等待佇列中獲取新的任務來執行。
  • 如果一個執行緒在指定的時間內一直沒有獲取到新任務,那麼我們會根據當前執行緒池當中活動的執行緒數量來決定是否銷燬它:如果當前執行緒池數量大於corePoolSize,那麼銷燬該執行緒,否則只有當設定了allowCoreThreadTimeOut,才會銷燬該執行緒。

二、ThreadPoolExecutor 實現

2.1 引數配置

從上面的處理流程可以看出,ThreadPoolExecutor對於任務的處理流程,會受到corePoolSize、等待佇列、maximumPoolSize等引數的影響,而這些引數都是可以由ThreadPoolExecutor的建立者去指定的,正是鑑於這種靈活性,使得我們僅僅通過簡單的配置就可以實現適用於不同的場景的ThreadPoolExecutor,下面,我們就來介紹一一介紹這些引數的含義:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
複製程式碼

(1) int corePoolSize

指定核心執行緒池的大小,當執行緒的數量沒有大於corePoolSize之前,始終會建立新執行緒來執行分配的任務。如果呼叫了preStartAllCoreThreads()方法,執行緒池會提前建立並啟動所有核心執行緒。

(2) int maximumPoolSize

指定執行緒池的最大數量,當加入新任務時,如果發現等待佇列已經滿了,那麼我們會嘗試通過建立新執行緒的方式來執行該任務,而如果此時執行緒池內執行緒的數量已經等於maximumPoolSize,那麼會採用指定的拒絕策略來處理該任務。

(3) long keepAliveTime 和 TimeUnit unit

當一個執行緒在執行完分配給它的任務之後,會嘗試從等待佇列中取出任務去執行,如果經過keepAliveTime之後仍然不能從佇列中獲取到任務,說明此時系統中可能並沒有那麼多的任務需要去處理,那麼就會根據執行緒池此時的狀態來決定是否銷燬該執行緒,以保證在能夠迅速響應任務的同時,又不至於有太多空閒的存活執行緒。

(4) BlockingQueue workQueue

指定等待佇列的實現方式,我們可以根據需要選擇以下幾種等待佇列:

  • ArrayBlockingQueue:基於陣列結構的有界等待佇列,按先進先出原則排序任務
  • LinkedBlockingQueue:基於連結串列結構的阻塞佇列,同樣按照先進先出原則排序任務,吞吐量要高於ArrayBlockingQueue
  • SynchronousQueue:對於這種阻塞佇列而言,每個插入操作必須要等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態。
  • PriorityBlockingQueue:一個具有優先順序的無限阻塞佇列。

(5) ThreadFactory threadFactory

用於建立執行緒的工廠。

(6) RejectedExecutionHandler handler

系統內建了以下幾種策略,用於佇列和執行緒池都滿了的情況:

  • AbortPolicy:丟擲異常,這也是預設的策略
  • CallerRunsPolicy:使用呼叫者所線上程來執行任務
  • DiscardOldestPolicy:先丟棄佇列中最末尾的任務,再重新通過execute方法執行該任務。
  • DiscardPolicy:不做任何處理,直接丟棄

2.2 內建 ThreadPoolExecutor

多執行緒知識梳理(5) - 執行緒池四部曲之 Executor 框架 中,我們介紹了幾種ThreadPoolExecutor的實現:

多執行緒知識梳理(6)   執行緒池四部曲之 ThreadPoolExecutor
它們其實都是ThreadPoolExecutor,只是在構造時傳入了不同的引數,如下表所示:
多執行緒知識梳理(6)   執行緒池四部曲之 ThreadPoolExecutor
結合之前對於處理流程和核心引數的分析,對它們進行進一步的介紹:

(1) FixedThreadPool

  • 傳入的nThread引數將被作為核心執行緒數和最大執行緒數,當執行緒池的數量達到nThread後,之後的任務將會被加入到無界的等待佇列當中
  • 除非某個執行緒因為異常而結束,否則當執行緒池的數量達到nThread之後將會一直保持不變
  • 由於使用的是無界佇列,因此執行緒池不會拒絕任務

(2) SingleThreadPoolExecutor

  • 如果當前執行緒池中無執行的執行緒時,將建立一個新執行緒來執行任務
  • 由於最大執行緒數被設定為1,因此之後的任務都被加入到無界佇列當中,並且由執行緒池中這個唯一的執行緒從等待佇列中,按照新增的順序依次執行任務

(3) CachedThreadPool

  • 由於等待佇列使用的是SynchonousQueue,它的每個插入操作都必須等待另一個執行緒的移除操作,對於執行緒池而言,也就是說:在新增任務到等待佇列時,必須要有一個空閒執行緒正在嘗試從等待佇列獲取任務,才有可能新增成功。
  • 因此,當一個任務被新增進入執行緒池時,會有以下兩種情況:
  • 如果當前有空閒執行緒正在嘗試從等待佇列中獲取任務,那麼這個任務將會被交給這個空閒執行緒進行處理
  • 如果當前沒有空閒執行緒嘗試從等待佇列中獲取任務,那麼將會建立一個新執行緒來執行任務
  • 由於設定了等待超時時間,因此某個執行緒在60s內都無法獲取到新的任務,那麼它將會被銷燬。

三、ThreadPoolExecutor 原始碼走讀

3.1 ctl

ThreadPoolExector中,有一個關鍵變數 - ctl,理解它是我們進行原始碼走讀的基礎。

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0))
複製程式碼

這個原子操作類包含了兩部分資訊:執行緒池的狀態執行緒池中的存活執行緒數目,它們用32位的整型數來表示,其中高3位表示執行緒池的狀態,低位表示當前執行緒池中存活的執行緒數。

在某一時刻,執行緒池會處於以下五種狀態之一:

  • RUNNINGAccept new tasks and process queued tasks
  • SHUTDOWNDon't accept new tasks, but process queued tasks
  • STOPDon't accept new tasks, don't process queued tasks, and interrupt in-progress tasks
  • TIDYINGAll tasks have terminated, workerCount is zero, the thread transitioning to state TIDYING will run the terminated() hook method
  • TERMINATEDterminated() has completed

這五種狀態之間轉換轉換圖為:

多執行緒知識梳理(6)   執行緒池四部曲之 ThreadPoolExecutor

對於ctl變數,以下三個函式可以用來拆解和組裝:

//獲取執行緒池的狀態資訊
private static int runStateOf(int c)     { return c & ~CAPACITY; }
//獲取執行緒池的存活執行緒數
private static int workerCountOf(int c)  { return c & CAPACITY; }
//將狀態資訊和存活執行緒數進行組合
private static int ctlOf(int rs, int wc) { return rs | wc; }
複製程式碼

3.2 任務執行過程

下面,我們通過模擬一個任務的執行來對ThreadPoolExecutor的原始碼進行簡單的走讀,整個流程如下圖所示,紅色字部分為我們所要關注的關鍵方法:

多執行緒知識梳理(6)   執行緒池四部曲之 ThreadPoolExecutor
(1) public void execute(Runnable command)

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        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);
        } else if (!addWorker(command, false)) {
            reject(command);
        }
    }
複製程式碼

前面我們說過,向一個執行緒池中提交任務有兩種方法,execute/submit,它們最終都會呼叫到上面的這個execute當中,這一函式的邏輯分為三步:

  • 當執行緒池中的存活執行緒數小於指定的核心執行緒數時,嘗試通過addWorker(firstTask, core)建立一個新的執行緒來執行任務,這裡將傳入的runnable作為該執行緒的第一個任務,並且core引數為true,如果建立成功,那麼直接返回,否則重新獲取一次ctl變數,跳轉到步驟2
  • 接著通過ctl變數判斷如果當前執行緒池處於running狀態,那麼將runnable新增到等待佇列workQueue當中,如果新增失敗跳轉到步驟3,新增成功則進行二次檢查,當發現了下面這兩種情況之一,那麼還需要進行額外的處理:
  • 如果發現執行緒池變為了非running狀態,那麼會將該任務從等待佇列中移除;
  • 如果當前執行緒池已經沒有存活的執行緒,那麼為了讓等待佇列中的任務可以執行,我們需要通過addWorker方法啟動一個新執行緒,與第一步不同的是,該執行緒的第一個任務為空。
  • 通過addWorker方法建立新執行緒來執行該任務,和第一步的唯一區別就是core引數為false,如果建立失敗,那麼執行拒絕策略。

(2) private boolean addWorker(Runnable firstTask, boolean core) 下面,我們再來看一下這個核心的函式addWorker,它的最終目的就是建立一個新的執行緒來執行任務:

   private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            //第一部分:當前執行緒池的狀態是否滿足加入的條件
            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;
                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;
        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());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) 
                            throw new IllegalThreadStateException();
                        //第四部分:將Worker加入到執行緒池中
                        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;
    }
複製程式碼

整個addWorker進行了以下幾步操作:

  • 根據當前執行緒池的狀態,判斷是否允許新建執行緒
  • 根據當前執行緒池的工作執行緒數,判斷是否允許新建執行緒
  • 建立一個Worker物件,這個Worker類中包含了一個執行緒
        Worker(Runnable firstTask) {
            setState(-1); 
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
複製程式碼
  • 將新建的Worker物件加入到執行緒池中
  • 啟動Worker中的執行緒

(3) final void runWorker(Worker w)

在第(2)步中,我們啟動了Worker物件中的執行緒t,它會呼叫Worker物件的run()函式,接著會執行runWorker方法:

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

這裡面的task就是我們通過execute方法傳入的Runnable,如果Worker的第一個任務不為空,那麼會首先執行該任務,如果第一個任務執行完畢,那麼會呼叫getTask()方法來嘗試去獲取下一個任務,當getTask()方法不返回(等待佇列為空)時,會一直阻塞在這裡,而當這個while迴圈退出的時候,那麼Worker所對應的執行緒就會被銷燬。

(4) private Runnable getTask()

private Runnable getTask() {
        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.
            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;
            }
        }
    }
複製程式碼

getTask()方法會去在第(1)步中的等待佇列workerQueue取任務,在獲取任務的時候會考慮超時時間keepAliveTime,如果超時時間到了仍然沒有獲取到任務,那麼getTask()方法就會返回null,從而runWorker()中的while迴圈就會結束,之後在finally程式碼塊中通過processWorkerExit(w, completedAbruptly)銷燬該執行緒。

四、關閉執行緒池

關閉執行緒池有兩種方法:shutdownshutdownNow

  • shutdown:將執行緒池的狀態設定成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的執行緒。
  • shutdownNow:將執行緒池的狀態設定為STOP,嘗試停止所有正在執行或暫停任務的執行緒,並返回等待執行任務的列表。

相關文章