Java執行緒池從使用到閱讀原始碼(3/10)

兜裡有辣條發表於2019-03-18

我們一般不會選擇直接使用執行緒類Thread進行多執行緒程式設計,而是使用更方便的執行緒池來進行任務的排程和管理。執行緒池就像共享單車,我們只要在我們有需要的時候去獲取就可以了。甚至可以說執行緒池更棒,我們只需要把任務提交給它,它就會在合適的時候執行了。但是如果直接使用Thread類,我們就需要在每次執行任務時自己建立、執行、等待執行緒了,而且很難對執行緒進行整體的管理,這可不是一件輕鬆的事情。既然我們已經有了執行緒池,那還是把這些麻煩事交給執行緒池來處理吧。

這篇文章將會從執行緒池的概念與一般使用入手,首先讓大家可以瞭解執行緒池的基本使用方法,之後會介紹實踐中最常用的四種執行緒池。最後,我們會通過對JDK原始碼的剖析深入瞭解執行緒池的執行過程和具體設計,真正達到知其然而知其所以然的水平。雖然只要瞭解了API就可以滿足一般的日常使用了,但是隻有當我們真正釐清了多執行緒相關的知識點,才能在面對多執行緒的實踐與面試問題時做到遊刃有餘、成竹在胸。

本文是一系列多執行緒文章中的第三篇,主要講解了執行緒池相關的知識,這個系列總共有十篇文章,前五篇暫定結構如下,感興趣的讀者可以關注一下:

  1. 併發基本概念——當我們在說“併發、多執行緒”,說的是什麼?
  2. 多執行緒入門——這一次,讓我們完全掌握Java多執行緒(2/10)
  3. 執行緒池使用與原理剖析——本文
  4. 執行緒同步機制
  5. 併發常見問題

執行緒池的使用方法

一般我們最常用的執行緒池實現類是ThreadPoolExecutor,我們接下來會介紹這個類的基本使用方法。JDK已經對執行緒池做了比較好的封裝,相信這個過程會非常輕鬆。

建立執行緒池

既然執行緒池是一個Java類,那麼最直接的使用方法一定是new一個ThreadPoolExecutor類的物件,例如ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>() )。那麼這個構造器的裡每個引數是什麼意思呢?

下面就是這個構造器的方法簽名:

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

各個引數分別表示下面的含義:

  1. corePoolSize,核心執行緒池大小,一般執行緒池會至少保持這麼多的執行緒數量;
  2. maximumPoolSize,最大執行緒池大小,也就是執行緒池最大的執行緒數量;
  3. keepAliveTime和unit共同組成了一個超時間,keepAliveTime是時間數量,unit是時間單位,單位加數量組成了最終的超時時間。這個超時時間表示如果執行緒池中包含了超過corePoolSize數量的執行緒,則在有執行緒空閒的時間超過了超時時間時該執行緒就會被銷燬;
  4. workQueue是任務的阻塞佇列,在沒有執行緒池中沒有足夠的執行緒可用的情況下會將任務先放入到這個阻塞佇列中等待執行。這裡傳入的佇列型別就決定了執行緒池在處理這些任務時的策略。

執行緒池中的阻塞佇列專門用於存放待執行的任務,在ThreadPoolExecutor中一個任務可以通過兩種方式被執行:第一種是直接在建立一個新的Worker時被作為第一個任務傳入,由這個新建立的執行緒來執行;第二種就是把任務放入一個阻塞佇列,等待執行緒池中的工作執行緒撈取任務進行執行。

上面提到的阻塞佇列是這樣的一種資料結構,它是一個佇列(類似於一個List),可以存放0到N個元素。我們可以對這個佇列進行插入和彈出元素的操作,彈出操作可以理解為是一個獲取並從佇列中刪除一個元素的操作。當佇列中沒有元素時,對這個佇列的獲取操作將會被阻塞,直到有元素被插入時才會被喚醒;當佇列已滿時,對這個佇列的插入操作將會被阻塞,直到有元素被彈出後才會被喚醒。這樣的一種資料結構非常適合於執行緒池的場景,當一個工作執行緒沒有任務可處理時就會進入阻塞狀態,直到有新任務提交後才被喚醒。

提交任務

當建立了一個執行緒池之後我們就可以將任務提交到執行緒池中執行了。提交任務到執行緒池中相當簡單,我們只要把原來傳入Thread類構造器的Runnable物件傳入執行緒池的execute方法或者submit方法就可以了。execute方法和submit方法基本沒有區別,兩者的區別只是submit方法會返回一個Future物件,用於檢查非同步任務的執行情況和獲取執行結果(非同步任務完成後)。

我們可以先試試如何使用比較簡單的execute方法,程式碼例子如下:

public class ThreadPoolTest {

    private static int count = 0;

    public static void main(String[] args) throws Exception {
        Runnable task = new Runnable() {
            public void run() {
                for (int i = 0; i < 1000000; ++i) {
                    synchronized (ThreadPoolTest.class) {
                        count += 1;
                    }
                }
            }
        };

        // 重要:建立執行緒池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 0L,
        TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

        // 重要:向執行緒池提交兩個任務
        threadPool.execute(task);
        threadPool.execute(task);

        // 等待執行緒池中的所有任務完成
        threadPool.shutdown();
        while (!threadPool.awaitTermination(1L, TimeUnit.MINUTES)) {
            System.out.println("Not yet. Still waiting for termination");
        }
        
        System.out.println("count = " + count);
    }
}
複製程式碼

關閉執行緒池

上面的程式碼中為了等待執行緒池中的所有任務執行完已經使用了shutdown()方法,關閉執行緒池的方法主要有兩個:

  1. shutdown(),有序關閉執行緒池,呼叫後執行緒池會讓已經提交的任務完成執行,但是不會再接受新任務。
  2. shutdownNow(),直接關閉執行緒池,執行緒池中正在執行的任務會被中斷,正在等待執行的任務不會再被執行,但是這些還在阻塞佇列中等待的任務會被作為返回值返回。

監控執行緒池執行狀態

我們可以通過呼叫執行緒池物件上的一些方法來獲取執行緒池當前的執行資訊,常用的方法有:

  • getTaskCount,執行緒池中已完成、執行中、等待執行的任務總數估計值。因為在統計過程中任務會發生動態變化,所以最後的結果並不是一個準確值;
  • getCompletedTaskCount,執行緒池中已完成的任務總數,這同樣是一個估計值;
  • getLargestPoolSize,執行緒池曾經建立過的最大執行緒數量。通過這個資料可以知道執行緒池是否充滿過,也就是達到過maximumPoolSize;
  • getPoolSize,執行緒池當前的執行緒數量;
  • getActiveCount,當前執行緒池中正在執行任務的執行緒數量估計值。

四種常用執行緒池

很多情況下我們也不會直接建立ThreadPoolExecutor類的物件,而是根據需要通過Executors的幾個靜態方法來建立特定用途的執行緒池。目前常用的執行緒池有四種:

  1. 可快取執行緒池,使用Executors.newCachedThreadPool方法建立
  2. 定長執行緒池,使用Executors.newFixedThreadPool方法建立
  3. 延時任務執行緒池,使用Executors.newScheduledThreadPool方法建立
  4. 單執行緒執行緒池,使用Executors.newSingleThreadExecutor方法建立

下面通過這些靜態方法的原始碼來具體瞭解一下不同型別執行緒池的特性與適用場景。

可快取執行緒池

JDK中的原始碼我們通過在IDE中進行跳轉可以很方便地進行檢視,下面就是Executors.newCachedThreadPool方法中的原始碼。從程式碼中我們可以看到,可快取執行緒池其實也是通過直接建立ThreadPoolExecutor類的構造器建立的,只是其中的引數都已經被設定好了,我們可以不用做具體的設定。所以我們要觀察的重點就是在這個方法中具體產生了一個怎樣配置的ThreadPoolExecutor物件,以及這樣的執行緒池適用於怎樣的場景。

從下面的程式碼中,我們可以看到,傳入ThreadPoolExecutor構造器的值有: - corePoolSize核心執行緒數為0,代表執行緒池中的執行緒數可以為0 - maximumPoolSize最大執行緒數為Integer.MAX_VALUE,代表執行緒池中最多可以有無限多個執行緒 - 超時時間設定為60秒,表示執行緒池中的執行緒在空閒60秒後會被回收 - 最後傳入的是一個SynchronousQueue型別的阻塞佇列,代表每一個新新增的任務都要馬上有一個工作執行緒進行處理

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

所以可快取執行緒池在新增任務時會優先使用空閒的執行緒,如果沒有就建立一個新執行緒,執行緒數沒有上限,所以每一個任務都會馬上被分配到一個工作執行緒進行執行,不需要在阻塞佇列中等待;如果執行緒池長期閒置,那麼其中的所有執行緒都會被銷燬,節約系統資源。

  • 優點
    • 任務在新增後可以馬上執行,不需要進入阻塞佇列等待
    • 在閒置時不會保留執行緒,可以節約系統資源
  • 缺點
    • 對執行緒數沒有限制,可能會過量消耗系統資源
  • 適用場景
    • 適用於大量短耗時任務和對響應時間要求較高的場景

定長執行緒池

傳入ThreadPoolExecutor構造器的值有:

  • corePoolSize核心執行緒數和maximumPoolSize最大執行緒數都為固定值nThreads,即執行緒池中的執行緒數量會保持在nThreads,所以被稱為“定長執行緒池”
  • 超時時間被設定為0毫秒,因為執行緒池中只有核心執行緒,所以不需要考慮超時釋放
  • 最後一個引數使用了無界佇列,所以在所有執行緒都在處理任務的情況下,可以無限新增任務到阻塞佇列中等待執行
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
複製程式碼

定長執行緒池中的執行緒數會逐步增長到nThreads個,並且在之後空閒執行緒不會被釋放,執行緒數會一直保持在nThreads個。如果新增任務時所有執行緒都處於忙碌狀態,那麼就會把任務新增到阻塞佇列中等待執行,阻塞佇列中任務的總數沒有上限。

  • 優點
    • 執行緒數固定,對系統資源的消耗可控
  • 缺點
    • 在任務量暴增的情況下執行緒池不會彈性增長,會導致任務完成時間延遲
    • 使用了無界佇列,線上程數設定過小的情況下可能會導致過多的任務積壓,引起任務完成時間過晚和資源被過度消耗的問題
  • 適用場景
    • 任務量峰值不會過高,且任務對響應時間要求不高的場景

延時任務執行緒池

與之前的兩個方法不同,Executors.newScheduledThreadPool返回的是ScheduledExecutorService介面物件,可以提供延時執行、定時執行等功能。線上程池配置上有如下特點:

  • maximumPoolSize最大執行緒數為無限,在任務量較大時可以建立大量新執行緒執行任務
  • 超時時間為0,執行緒空閒後會被立即銷燬
  • 使用了延時工作佇列,延時工作佇列中的元素都有對應的過期時間,只有過期的元素才會被彈出
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}
複製程式碼

延時任務執行緒池實現了ScheduledExecutorService介面,主要用於需要延時執行和定時執行的情況。

單執行緒執行緒池

單執行緒執行緒池中只有一個工作執行緒,可以保證新增的任務都以指定順序執行(先進先出、後進先出、優先順序)。但是如果執行緒池裡只有一個執行緒,為什麼我們還要用執行緒池而不直接用Thread呢?這種情況下主要有兩種優點:一是我們可以通過共享的執行緒池很方便地提交任務進行非同步執行,而不用自己管理執行緒的生命週期;二是我們可以使用任務佇列並指定任務的執行順序,很容易做到任務管理的功能。

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

執行緒池的內部實現

通過前面的內容我們其實已經可以在程式碼中使用執行緒池了,但是我們為什麼還要去深究執行緒池的內部實現呢?首先,可能有一個很功利性的目的就是為了面試,在面試時如果能準確地說出一些底層的執行機制與原理那一定可以成為過程中一個重要的亮點。

但是我認為學習探究執行緒池的內部實現的作用絕對不僅是如此,只有深入瞭解並釐清了執行緒池的具體實現,我們才能解決實踐中需要考慮的各種邊界條件。因為多執行緒程式設計所代表的併發程式設計並不是一個固定的知識點,而是實踐中不斷在發展和完善的一個知識門類。我們也許會需要同時考慮多個維度,最後得到一個特定於應用場景的解決方案,這就要求我們具備從細節著手構建出解決方案並做好各個考慮維度之間的取捨的能力。

而且我相信只要在某一個點上能突破到相當的深度,那麼以後從這個點上向外擴充套件就會容易得多。也許在剛開始我們的探究會碰到非常大的阻力,但是我們要相信,最後我們可以得到的將不止是一個知識點而是一整個知識面。

檢視JDK原始碼的方式

在IDE中,例如IDEA裡,我們可以點選我們樣例程式碼裡的ThreadPoolExecutor類跳轉到JDK中ThreadPoolExecutor類的原始碼。在原始碼中我們可以看到很多java.util.concurrent包的締造者大牛“Doug Lea”所留下的各種註釋,下面的圖片就是該類原始碼的一個截圖。

Java執行緒池從使用到閱讀原始碼(3/10)

這些註釋的內容非常有參考價值,建議有能力的讀者朋友可以自己閱讀一遍。下面,我們就一步步地抽絲剝繭,來揭開執行緒池類ThreadPoolExecutor原始碼的神祕面紗。

控制變數與執行緒池生命週期

ThreadPoolExecutor類定義的開頭,我們可以看到如下的幾行程式碼:

// 控制變數,前3位表示狀態,剩下的資料位表示有效的執行緒數
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// Integer的位數減去3位狀態位就是執行緒數的位數
private static final int COUNT_BITS = Integer.SIZE - 3;
// CAPACITY就是執行緒數的上限(含),即2^COUNT_BITS - 1個
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
複製程式碼

第一行是一個用來作為控制變數的整型值,即一個Integer。之所以要用AtomicInteger類是因為要保證多執行緒安全,在本系列之後的文章中會對AtomicInteger進行具體介紹。一個整型一般是32位,但是這裡的程式碼為了保險起見,還是使用了Integer.SIZE來表示整型的總位數。這裡的“位”指的是資料位(bit),在計算機中,8bit = 1位元組,1024位元組 = 1KB,1024KB = 1MB。每一位都是一個0或1的數字,我們如果把整型想象成一個二進位制(0或1)的陣列,那麼一個Integer就是32個數字的陣列。其中,前三個被用來表示狀態,那麼我們就可以表示2^3 = 8個不同的狀態了。剩下的29位二進位制數字都會被用於表示當前執行緒池中有效執行緒的數量,上限就是(2^29 - 1)個,即常量CAPACITY

之後的部分列出了執行緒池的所有狀態:

private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;
複製程式碼

在這裡可以忽略數字後面的<< COUNT_BITS,可以把狀態簡單地理解為前面的數字部分,這樣的簡化基本不影響結論。

各個狀態的解釋如下:

  • RUNNING,正常執行狀態,可以接受新的任務和處理佇列中的任務
  • SHUTDOWN,關閉中狀態,不能接受新任務,但是可以處理佇列中的任務
  • STOP,停止中狀態,不能接受新任務,也不處理佇列中的任務,會中斷進行中的任務
  • TIDYING,待結束狀態,所有任務已經結束,執行緒數歸0,進入TIDYING狀態後將會執行terminated()方法
  • TERMINATED,結束狀態,terminated()方法呼叫完成後進入

這幾個狀態所對應的數字值是按照順序排列的,也就是說執行緒池的狀態只能從小到大變化,這也方便了通過數字比較來判斷狀態所在的階段,這種通過數字大小來比較狀態值的方法在ThreadPoolExecutor的原始碼中會有大量的使用。

下圖是這五個狀態之間的變化過程:

Java執行緒池從使用到閱讀原始碼(3/10)

  1. 當執行緒池被建立時會處於RUNNING狀態,正常接受和處理任務;
  2. shutdown()方法被直接呼叫,或者線上程池物件被GC回收時通過finalize()方法隱式呼叫了shutdown()方法時,執行緒池會進入SHUTDOWN狀態。該狀態下執行緒池仍然會繼續執行完阻塞佇列中的任務,只是不再接受新的任務了。當佇列中的任務被執行完後,執行緒池中的執行緒也會被回收。當佇列和執行緒都被清空後,執行緒池將進入TIDYING狀態;
  3. 線上程池處於RUNNING或者SHUTDOWN狀態時,如果有程式碼呼叫了shutdownNow()方法,則執行緒池會進入STOP狀態。在STOP狀態下,執行緒池會直接清空阻塞佇列中待執行的任務,然後中斷所有正在進行中的任務並回收執行緒。當執行緒都被清空以後,執行緒池就會進入TIDYING狀態;
  4. 當執行緒池進入TIDYING狀態時,將會執行terminated()方法,該方法執行完後,執行緒池就會進入最終的TERMINATED狀態,徹底結束。

到這裡我們就已經清楚地瞭解了執行緒從剛被建立時的RUNNING狀態一直到最終的TERMINATED狀態的整個生命週期了。那麼當我們要向一個RUNNING狀態的執行緒池提交任務時會發生些什麼呢?

execute方法的實現

我們一般會使用execute方法提交我們的任務,那麼執行緒池在這個過程中做了什麼呢?在ThreadPoolExecutor類的execute()方法的原始碼中,我們主要做了四件事:

  1. 如果當前執行緒池中的執行緒數小於核心執行緒數corePoolSize,則建立一個新的Worker代表一個執行緒,並把入參中的任務作為第一個任務傳入Worker。addWorker方法中的第一個引數是該執行緒的第一個任務,而第二個引數就是代表是否建立的是核心執行緒,在execute方法中addWorker總共被呼叫了三次,其中第一次傳入的是true,後兩次傳入的都是false;
  2. 如果當前執行緒池中的執行緒數已經滿足了核心執行緒數corePoolSize,那麼就會通過workQueue.offer()方法將任務新增到阻塞佇列中等待執行;
  3. 如果執行緒數已經達到了corePoolSize且阻塞佇列中無法插入該任務(比如已滿),那麼執行緒池就會再增加一個執行緒來執行該任務,除非執行緒數已經達到了最大執行緒數maximumPoolSize;
  4. 如果確實已經達到了最大執行緒數,那麼就拒絕這個任務。

總體上的執行流程如下,下方的黑色同心圓代表流程結束:

Java執行緒池從使用到閱讀原始碼(3/10)
這裡再重複一次阻塞佇列的定義,方便大家閱讀:

執行緒池中的阻塞佇列專門用於存放待執行的任務,在ThreadPoolExecutor中一個任務可以通過兩種方式被執行:第一種是直接在建立一個新的Worker時被作為第一個任務傳入,由這個新建立的執行緒來執行;第二種就是把任務放入一個阻塞佇列,等待執行緒池中的工作執行緒撈取任務進行執行。

上面提到的阻塞佇列是這樣的一種資料結構,它是一個佇列(類似於一個List),可以存放0到N個元素。我們可以對這個佇列進行插入和彈出元素的操作,彈出操作可以理解為是一個獲取並從佇列中刪除一個元素的操作。當佇列中沒有元素時,對這個佇列的獲取操作將會被阻塞,直到有元素被插入時才會被喚醒;當佇列已滿時,對這個佇列的插入操作將會被阻塞,直到有元素被彈出後才會被喚醒。這樣的一種資料結構非常適合於執行緒池的場景,當一個工作執行緒沒有任務可處理時就會進入阻塞狀態,直到有新任務提交後才被喚醒。

下面是帶有註釋的原始碼,大家可以和上面的流程對照起來參考一下:

public void execute(Runnable command) {
    // 檢查提交的任務是否為空
    if (command == null)
        throw new NullPointerException();
    
    // 獲取控制變數值
    int c = ctl.get();
    // 檢查當前執行緒數是否達到了核心執行緒數
    if (workerCountOf(c) < corePoolSize) {
        // 未達到核心執行緒數,則建立新執行緒
        // 並將傳入的任務作為該執行緒的第一個任務
        if (addWorker(command, true))
            // 新增執行緒成功則直接返回,否則繼續執行
            return;

        // 因為前面呼叫了耗時操作addWorker方法
        // 所以執行緒池狀態有可能發生了改變,重新獲取狀態值
        c = ctl.get();
    }

    // 判斷執行緒池當前狀態是否是執行中
    // 如果是則呼叫workQueue.offer方法將任務放入阻塞佇列
    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);
}
複製程式碼

addWorker方法

在前面execute方法的程式碼中我們可以看到執行緒池是通過addWorker方法來向執行緒池中新增新執行緒的,那麼新的執行緒又是如何執行起來的呢?

這裡我們暫時跳過addWorker方法的詳細原始碼,因為雖然這個方法的程式碼行數較多,但是功能相對比較直接,只是建立一個代表執行緒的Worker類物件,並呼叫這個物件所對應執行緒物件的start()方法。我們知道一旦呼叫了Thread類的start()方法,則這個執行緒就會開始呼叫建立執行緒時傳入的Runnable物件。從下面的Worker類構造器原始碼可以看出,Worker類正是把自己(this指標)傳入了執行緒的構造器當中,那麼這個執行緒就會執行Worker類的run()方法了,這個run()方法只執行了一行很簡單的程式碼runWorker(this);

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

public void run() {
    runWorker(this);
}
複製程式碼

runWorker方法的實現

我們看到執行緒池中的執行緒在啟動時會呼叫對應的Worker類的runWorker方法,而這裡就是整個執行緒池任務執行的核心所在了。runWorker方法中包含有一個類似無限迴圈的while語句,讓worker物件可以不斷執行提交到執行緒池中的新任務。

大家可以配合程式碼上帶有的註釋來理解該方法的具體實現:

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    // 將worker的狀態重置為正常狀態,因為state狀態值在構造器中被初始化為-1
    w.unlock();
    // 通過completedAbruptly變數的值判斷任務是否正常執行完成
    boolean completedAbruptly = true;
    try {
        // 如果task為null就通過getTask方法獲取阻塞佇列中的下一個任務
        // getTask方法一般不會返回null,所以這個while類似於一個無限迴圈
        // worker物件就通過這個方法的持續執行來不斷處理新的任務
        while (task != null || (task = getTask()) != null) {
            // 每一次任務的執行都必須獲取鎖來保證下方臨界區程式碼的執行緒安全
            w.lock();
            
            // 如果狀態值大於等於STOP(狀態值是有序的,即STOP、TIDYING、TERMINATED)
            // 且當前執行緒還沒有被中斷,則主動中斷執行緒
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();

            // 開始
            try {
                // 執行任務前處理操作,預設是一個空實現
                // 在子類中可以通過重寫來改變任務執行前的處理行為
                beforeExecute(wt, task);

                // 通過thrown變數儲存任務執行過程中丟擲的異常
                // 提供給下面finally塊中的afterExecute方法使用
                Throwable thrown = null;
                try {
                    // *** 重要:實際執行任務的程式碼
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    // 因為Runnable介面的run方法中不能丟擲Throwable物件
                    // 所以要包裝成Error物件丟擲
                    thrown = x; throw new Error(x);
                } finally {
                    // 執行任務後處理操作,預設是一個空實現
                    // 在子類中可以通過重寫來改變任務執行後的處理行為
                    afterExecute(task, thrown);
                }
            } finally {
                // 將迴圈變數task設定為null,表示已處理完成
                task = null;
                // 累加當前worker已經完成的任務數
                w.completedTasks++;
                // 釋放while體中第一行獲取的鎖
                w.unlock();
            }
        }

        // 將completedAbruptly變數設定為false,表示任務正常處理完成
        completedAbruptly = false;
    } finally {
        // 銷燬當前的worker物件,並完成一些諸如完成任務數量統計之類的輔助性工作
        // 線上程池當前狀態小於STOP的情況下會建立一個新的worker來替換被銷燬的worker
        processWorkerExit(w, completedAbruptly);
    }
}
複製程式碼

runWorker方法的原始碼中有兩個比較重要的方法呼叫,一個是while條件中對getTask方法的呼叫,一個是在方法的最後對processWorkerExit方法的呼叫。下面是對這兩個方法更詳細的解釋。

getTask方法在阻塞佇列中有待執行的任務時會從佇列中彈出一個任務並返回,如果阻塞佇列為空,那麼就會阻塞等待新的任務提交到佇列中直到超時(在一些配置下會一直等待而不超時),如果在超時之前獲取到了新的任務,那麼就會將這個任務作為返回值返回。

getTask方法返回null時會導致當前Worker退出,當前執行緒被銷燬。在以下情況下getTask方法才會返回null:

  1. 當前執行緒池中的執行緒數超過了最大執行緒數。這是因為執行時通過呼叫setMaximumPoolSize修改了最大執行緒數而導致的結果;
  2. 執行緒池處於STOP狀態。這種情況下所有執行緒都應該被立即回收銷燬;
  3. 執行緒池處於SHUTDOWN狀態,且阻塞佇列為空。這種情況下已經不會有新的任務被提交到阻塞佇列中了,所以執行緒應該被銷燬;
  4. 執行緒可以被超時回收的情況下等待新任務超時。執行緒被超時回收一般有以下兩種情況:
    • 超出核心執行緒數部分的執行緒等待任務超時
    • 允許核心執行緒超時(執行緒池配置)的情況下執行緒等待任務超時

processWorkerExit方法會銷燬當前執行緒對應的Worker物件,並執行一些累加總處理任務數等輔助操作。但線上程池當前狀態小於STOP的情況下會建立一個新的Worker來替換被銷燬的Worker,有興趣的讀者可以自行參考processWorkerExit方法原始碼。

總結

到這裡我們的執行緒池原始碼之旅就結束了,希望大家在看完這篇文章之後能對執行緒池的使用和執行都有一個大概的印象。為什麼說只是有了一個大概的印象呢?因為我覺得很多沒有相關基礎的讀者讀到這裡可能還只是對執行緒池有了一個自己的認識,對其中的一些細節可能還沒有完全捕捉到。所以我建議大家在看完下面的總結之後不妨再返回到文章的開頭多讀幾遍,相信第二遍的閱讀能給大家帶來不一樣的體驗,因為我自己也是在第三次讀ThreadPoolExecutor類的原始碼時才真正打通了其中的一些重要關節的。

在這篇文章中我們從執行緒池的概念和基本使用方法說起,然後介紹了ThreadPoolExecutor的構造器引數和常用的四種具體配置。最後的一大半篇幅我們一起在TheadPoolExecutor類的原始碼中暢遊了一番,瞭解了從執行緒池的建立到任務執行的完整執行模型。

引子

在瀏覽ThreadPoolExexutor原始碼的過程中,有幾個點我們其實並沒有完全說清楚,比如對鎖的加鎖操作、對控制變數的多次獲取、控制變數的AtomicInteger型別。在下一篇文章中,我將會介紹這些以鎖、volatile變數、CAS操作、AQS抽象類為代表的一系列執行緒同步方法,歡迎感興趣的讀者繼續關注我後續釋出的文章~

相關文章