深入理解執行緒池原理篇

三好碼農發表於2019-03-13

現在CPU都是有多個核心,並行已經成為事實,一方面我們希望最大限度利用機器效能(利用多執行緒提高吞吐率),另一方面機器的硬體資源是有限的,我們也不能無限制的去申請,幸運的是,JDK已經為我們提供了ExecutorService的實現,還提供了Executors工廠類方便我們生成模板執行緒池,但是簡單背後一定是複雜,這篇文章就是深入執行緒池的原始碼去一探究竟。

開始之前,需要明確幾個概念,方便後面理解執行緒池的執行原理。

核心執行緒(corePool):執行緒池最終執行任務的角色肯定還是執行緒,同時我們也會限制執行緒的數量,所以我們可以這樣理解核心執行緒,有新任務提交時,首先檢查核心執行緒數,如果核心執行緒都在工作,而且數量也已經達到最大核心執行緒數,則不會繼續新建核心執行緒,而會將任務放入等待佇列

等待佇列 (workQueue):等待佇列用於儲存當核心執行緒都在忙時,繼續新增的任務,核心執行緒在執行完當前任務後,也會去等待佇列拉取任務繼續執行,這個佇列一般是一個執行緒安全的阻塞佇列,它的容量也可以由開發者根據業務來定製。

非核心執行緒當等待佇列滿了,如果當前執行緒數沒有超過最大執行緒數,則會新建執行緒執行任務,那麼核心執行緒和非核心執行緒到底有什麼區別呢?說出來你可能不信,本質上它們沒有什麼區別,建立出來的執行緒也根本沒有標識去區分它們是核心還是非核心的,執行緒池只會去判斷已有的執行緒數(包括核心和非核心)去跟核心執行緒數和最大執行緒數比較,來決定下一步的策略

執行緒活動保持時間 (keepAliveTime):執行緒空閒下來之後,保持存活的持續時間,超過這個時間還沒有任務執行,該工作執行緒結束。

飽和策略 (RejectedExecutionHandler):當等待佇列已滿,執行緒數也達到最大執行緒數時,執行緒池會根據飽和策略來執行後續操作,預設的策略是拋棄要加入的任務。

一圖剩千言,上一張圖概括執行緒池的基本運作流程。

執行緒池運作概覽.png

按我的習慣,先提出幾個問題,然後帶著問題去尋找答案。
  1. 執行緒池的執行緒是如何做到複用的。
  2. 執行緒池是如何做到高效併發的。
  3. 從執行緒池的設計中,我們能學到什麼?

ThreadPoolExecutor

JDK中執行緒池的核心實現類是ThreadPoolExecutor,先看這個類的第一個成員變數ctl,AtomicInteger這個類可以通過CAS達到無鎖併發,效率比較高,這個變數有雙重身份,它的高三位表示執行緒池的狀態,低29位表示執行緒池中現有的執行緒數,這也是Doug Lea一個天才的設計,用最少的變數來減少鎖競爭,提高併發效率。

    //CAS,無鎖併發
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    //表示執行緒池執行緒數的bit數
    private static final int COUNT_BITS = Integer.SIZE - 3;
    //最大的執行緒數量,數量是完全夠用了
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    // runState is stored in the high-order bits
    //1110 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int RUNNING    = -1 << COUNT_BITS;
    //0000 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    //0010 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int STOP       =  1 << COUNT_BITS;
    //0100 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int TIDYING    =  2 << COUNT_BITS;
    //0110 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int TERMINATED =  3 << COUNT_BITS;

    // 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; }

    /*
     * Bit field accessors that don't require unpacking ctl.
     * These depend on the bit layout and on workerCount being never negative.
     * 判斷狀態c是否比s小,下面會給出狀態流轉圖
     */
    
    private static boolean runStateLessThan(int c, int s) {
        return c < s;
    }
    
    //判斷狀態c是否不小於狀態s
    private static boolean runStateAtLeast(int c, int s) {
        return c >= s;
    }
    //判斷執行緒是否在執行
    private static boolean isRunning(int c) {
        return c < SHUTDOWN;
    }
複製程式碼

關於執行緒池的狀態,有5種,

  1. RUNNING, 執行狀態,值也是最小的,剛建立的執行緒池就是此狀態。
  2. SHUTDOWN,停工狀態,不再接收新任務,已經接收的會繼續執行
  3. STOP,停止狀態,不再接收新任務,已經接收正在執行的,也會中斷
  4. 清空狀態,所有任務都停止了,工作的執行緒也全部結束了
  5. TERMINATED,終止狀態,執行緒池已銷燬

它們的流轉關係如下:

執行緒狀態流轉.png

execute/submit

向執行緒池提交任務有這2種方式,execute是ExecutorService介面定義的,submit有三種方法過載都在AbstractExecutorService中定義,都是將要執行的任務包裝為FutureTask來提交,使用者可以通過FutureTask來拿到任務的執行狀態和執行最終的結果,最終呼叫的都是execute方法,其實對於執行緒池來說,它並不關心你是哪種方式提交的,因為任務的狀態是由FutureTask自己維護的,對執行緒池透明

    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

    public <T> Future<T> submit(Runnable task, T result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }

    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }
複製程式碼

重點看execute的實現

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        //第一步,獲取ctl
        int c = ctl.get();
        //檢查當前執行緒數是否達到核心執行緒數的限制,注意執行緒本身是不區分核心還是非核心,後面會進一步驗證
        if (workerCountOf(c) < corePoolSize) {
            //如果核心執行緒數未達到,會直接新增一個核心執行緒,也就是說線上程池剛啟動預熱階段,
            //提交任務後,會優先啟動核心執行緒處理
            if (addWorker(command, true))
                return;
            //如果新增任務失敗,重新整理ctl,進入下一步
            c = ctl.get();
        }
        //檢查執行緒池是否是執行狀態,然後將任務新增到等待佇列,注意offer是不會阻塞的
        if (isRunning(c) && workQueue.offer(command)) {
           //任務成功新增到等待佇列,再次重新整理ctl
            int recheck = ctl.get();
           //如果執行緒池不是執行狀態,則將剛新增的任務從佇列移除並執行拒絕策略
            if (! isRunning(recheck) && remove(command))
                reject(command);
            //判斷當前執行緒數量,如果執行緒數量為0,則新增一個非核心執行緒,並且不指定首次執行任務
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
       //新增非核心執行緒,指定首次執行任務,如果新增失敗,執行異常策略
        else if (!addWorker(command, false))
            reject(command);
    }
    
    /*
     * addWorker方法申明
     * @param core if true use corePoolSize as bound, else
     * maximumPoolSize. (A boolean indicator is used here rather than a
     * value to ensure reads of fresh values after checking other pool
     * state).
     * @return true if successful
     */
    private boolean addWorker(Runnable firstTask, boolean core) {
    //.....
    }
複製程式碼

這裡有2個細節,可以深挖一下。

  1. 可以看到execute方法中沒有用到重量級鎖,ctl雖然可以保證本身變化的原子性,但是不能保證方法內部的程式碼塊的原子性,是否會有併發問題?
  2. 上面提到過,addWorker方法可以新增工作執行緒(核心或者非核心),執行緒本身沒有核心或者非核心的標識,core引數只是用來確定 當前執行緒數的比較物件是執行緒池設定的核心執行緒數還是最大執行緒數,真實情況是不是這樣?

addWorker

新增執行緒的核心方法,直接看原始碼

private boolean addWorker(Runnable firstTask, boolean core) {
       //相當於goto,雖然不建議濫用,但這裡使用又覺得沒一點問題
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
            //如果執行緒池的狀態到了SHUTDOWN或者之上的狀態時候,只有一種情況還需要繼續新增執行緒,
            //那就是執行緒池已經SHUTDOWN,但是佇列中還有任務在排隊,而且不接受新任務(所以firstTask必須為null)
           //這裡還繼續新增執行緒的初衷是,加快執行等待佇列中的任務,儘快讓執行緒池關閉
            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
               //傳入的core的引數,唯一用到的地方,如果執行緒數超過理論最大容量,如果core是true跟最大核心執行緒數比較,否則跟最大執行緒數比較
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                //通過CAS自旋,增加執行緒數+1,增加成功跳出雙層迴圈,繼續往下執行
                if (compareAndIncrementWorkerCount(c))
                    break retry;
               //檢測當前執行緒狀態如果發生了變化,則繼續回到retry,重新開始迴圈
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
        //走到這裡,說明我們已經成功的將執行緒數+1了,但是真正的執行緒還沒有被新增
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
           //新增執行緒,Worker是繼承了AQS,實現了Runnable介面的包裝類
            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());
                    //檢查執行緒狀態,還是跟之前一樣,只有當執行緒池處於RUNNING,或者處於SHUTDOWN並且firstTask==null的時候,這時候建立Worker來加速處理佇列中的任務
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                       //執行緒只能被start一次
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                      //workers是一個HashSet,新增我們新增的Worker
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                  //啟動Worker
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }
複製程式碼

分析完addWorker的原始碼實現,我們可以回答上面留下的二個疑問,

  1. execute方法雖然沒有加鎖,但是在addWorker方法內部,加鎖了,這樣可以保證不會建立超過我們預期的執行緒數,大師在設計的時候,做到了在最小的範圍內加鎖,儘量減少鎖競爭,
  2. 可以看到,core引數,只是用來判斷當前執行緒數是否超量的時候跟corePoolSize還是maxPoolSize比較,Worker本身無核心或者非核心的概念。 ####繼續看Worker是怎麼工作的
//Worker的run方法呼叫的是ThreadPoolExecutor的runWorker方法
    public void run() {
          runWorker(this);
    }


    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        //取出需要執行的任務,
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            //如果task不是null,或者去佇列中取任務,注意這裡會阻塞,後面會分析getTask方法
            while (task != null || (task = getTask()) != null) {
               //這個lock在這裡是為了如果執行緒被中斷,那麼會丟擲InterruptedException,而退出迴圈,結束執行緒
                w.lock();
                //判斷執行緒是否需要中斷
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                   //任務開始執行前的hook方法
                    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 {
                       ////任務開始執行後的hook方法
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
           //Worker退出
            processWorkerExit(w, completedAbruptly);
        }
    }

   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.
           //檢查執行緒池的狀態,如果已經是STOP及以上的狀態,或者已經SHUTDOWN,佇列也是空的時候,直接return null,並將Worker數量-1
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

           // 注意這裡的allowCoreThreadTimeOut引數,字面意思是否允許核心執行緒超時,即如果我們設定為false,那麼只有當執行緒數wc大於corePoolSize的時候才會超時
           //更直接的意思就是,如果設定allowCoreThreadTimeOut為false,那麼執行緒池在達到corePoolSize個工作執行緒之前,不會讓閒置的工作執行緒退出
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
          //確認超時,將Worker數-1,然後返回
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
                //從佇列中取任務,根據timed選擇是有時間期限的等待還是無時間期限的等待
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }
複製程式碼

現在我們可以回答文章一開始提出的三個問題中的前2個了

  1. 執行緒池的執行緒是如何做到複用的。 執行緒池中的執行緒在迴圈中嘗試取任務執行,這一步會被阻塞,如果設定了allowCoreThreadTimeOut為true,則執行緒池中的所有執行緒都會在keepAliveTime時間超時後還未取到任務而退出。或者執行緒池已經STOP,那麼所有執行緒都會被中斷,然後退出。
  2. 執行緒池是如何做到高效併發的。 看整個執行緒池的工作流程,有以下幾個需要特別關注的併發點. ①: 執行緒池狀態和工作執行緒數量的變更。這個由一個AtomicInteger變數 ctl來解決原子性問題。 ②: 向工作Worker容器workers中新增新的Worker的時候。這個執行緒池本身已經加鎖了。 ③: 工作執行緒Worker從等待佇列中取任務的時候。這個由工作佇列本身來保證執行緒安全,比如LinkedBlockingQueue等。

怎麼用好Executors

JDK已經給我們提供了很方便的執行緒池工廠類Executors, 方便我們快速建立執行緒池,可能在閱讀原始碼之前,我們在面對具體的業務場景時,到底該選擇哪種執行緒池配置是有疑問的,我們來看一下.

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
複製程式碼

newFixedThreadPool, 可以看到我們需要傳入一個執行緒數量的引數nThreads,這樣執行緒池的核心執行緒數和最大執行緒數都會設成nThreads, 而它的等待佇列是一個LinkedBlockingQueue,它的容量限制是Integer.MAX_VALUE, 可以認為是沒有邊界的。核心執行緒keepAlive時間0,allowCoreThreadTimeOut預設false。所以這個方法建立的執行緒池適合能估算出需要多少核心執行緒數量的場景。

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
複製程式碼

newSingleThreadExecutor, 有且只有一個執行緒在工作,適合任務順序執行,缺點但是不能充分利用CPU多核效能

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
複製程式碼

newCachedThreadPool, 核心執行緒數0,最大執行緒數Integer.MAX_VALUE, 執行緒keepAlive時間60s,用的佇列是SynchronousQueue,這種佇列本身不會存任務,只做轉發,所以newCachedThreadPool適合執行大量的,輕量級任務。

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
複製程式碼

newScheduledThreadPool, 執行週期性任務,類似定時器。

最後的問題:從執行緒池的設計中,我們能學到什麼?

以我的個人體會,大概有下面四點

  1. 清楚實現原理,可以指導我們更好的使用。
  2. 在寫併發程式的時候,儘可能的縮小鎖的範圍,提高程式碼的吞吐率。
  3. goto,不是一定不能用,而不是濫用,有些場景有奇效。
  4. 如果你需要多個執行緒安全的int型變數,考慮利用位運算把它們合併為一個。

全文完,水平有限,有疑問,歡迎交流!

相關文章