執行緒池之ScheduledThreadPoolExecutor

你聽___發表於2018-05-06

原創文章&經驗總結&從校招到 A 廠一路陽光一路滄桑

詳情請戳www.codercc.com

1. ScheduledThreadPoolExecutor 簡介

ScheduledThreadPoolExecutor 可以用來在給定延時後執行非同步任務或者週期性執行任務,相對於任務排程的 Timer 來說,其功能更加強大,Timer 只能使用一個後臺執行緒執行任務,而 ScheduledThreadPoolExecutor 則可以通過建構函式來指定後臺執行緒的個數。ScheduledThreadPoolExecutor 類的 UML 圖如下:

ScheduledThreadPoolExecutor類的UML圖.png
ScheduledThreadPoolExecutor類的UML圖.png
  1. 從 UML 圖可以看出,ScheduledThreadPoolExecutor 繼承了ThreadPoolExecutor,也就是說 ScheduledThreadPoolExecutor 擁有 execute()和 submit()提交非同步任務的基礎功能,關於 ThreadPoolExecutor可以看這篇文章。但是,ScheduledThreadPoolExecutor 類實現了ScheduledExecutorService,該介面定義了 ScheduledThreadPoolExecutor 能夠延時執行任務和週期執行任務的功能;
  2. ScheduledThreadPoolExecutor 也兩個重要的內部類:DelayedWorkQueueScheduledFutureTask。可以看出 DelayedWorkQueue 實現了 BlockingQueue 介面,也就是一個阻塞佇列,ScheduledFutureTask 則是繼承了 FutureTask 類,也表示該類用於返回非同步任務的結果。這兩個關鍵類,下面會具體詳細來看。

1.1 構造方法

ScheduledThreadPoolExecutor 有如下幾個構造方法:

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

可以看出由於 ScheduledThreadPoolExecutor 繼承了 ThreadPoolExecutor,它的構造方法實際上是呼叫了 ThreadPoolExecutor,對 ThreadPoolExecutor 的介紹可以可以看這篇文章,理解 ThreadPoolExecutor 構造方法的幾個引數的意義後,理解這就很容易了。可以看出,ScheduledThreadPoolExecutor 的核心執行緒池的執行緒個數為指定的 corePoolSize,當核心執行緒池的執行緒個數達到 corePoolSize 後,就會將任務提交給有界阻塞佇列 DelayedWorkQueue,對 DelayedWorkQueue 在下面進行詳細介紹,執行緒池允許最大的執行緒個數為 Integer.MAX_VALUE,也就是說理論上這是一個大小無界的執行緒池。

1.2 特有方法

ScheduledThreadPoolExecutor 實現了ScheduledExecutorService介面,該介面定義了可延時執行非同步任務和可週期執行非同步任務的特有功能,相應的方法分別為:

//達到給定的延時時間後,執行任務。這裡傳入的是實現Runnable介面的任務,
//因此通過ScheduledFuture.get()獲取結果為null
public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);
//達到給定的延時時間後,執行任務。這裡傳入的是實現Callable介面的任務,
//因此,返回的是任務的最終計算結果
 public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay, TimeUnit unit);
//是以上一個任務開始的時間計時,period時間過去後,
//檢測上一個任務是否執行完畢,如果上一個任務執行完畢,
//則當前任務立即執行,如果上一個任務沒有執行完畢,則需要等上一個任務執行完畢後立即執行
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);
//當達到延時時間initialDelay後,任務開始執行。上一個任務執行結束後到下一次
//任務執行,中間延時時間間隔為delay。以這種方式,週期性執行任務。
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);
複製程式碼

2. 可週期性執行的任務---ScheduledFutureTask

ScheduledThreadPoolExecutor 最大的特色是能夠週期性執行非同步任務,當呼叫schedule,scheduleAtFixedRate和scheduleWithFixedDelay方法時,實際上是將提交的任務轉換成的 ScheduledFutureTask 類,從原始碼就可以看出。以 schedule 方法為例:

public ScheduledFuture<?> schedule(Runnable command,
                                   long delay,
                                   TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    RunnableScheduledFuture<?> t = decorateTask(command,
        new ScheduledFutureTask<Void>(command, null,
                                      triggerTime(delay, unit)));
    delayedExecute(t);
    return t;
}
複製程式碼

可以看出,通過decorateTask會將傳入的 Runnable 轉換成ScheduledFutureTask類。執行緒池最大作用是將任務和執行緒進行解耦,執行緒主要是任務的執行者,而任務也就是現在所說的 ScheduledFutureTask。緊接著,會想到任何執行緒執行任務,總會呼叫run()方法。為了保證 ScheduledThreadPoolExecutor 能夠延時執行任務以及能夠週期性執行任務,ScheduledFutureTask 重寫了 run 方法:

public void run() {
    boolean periodic = isPeriodic();
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    else if (!periodic)
		//如果不是週期性執行任務,則直接呼叫run方法
        ScheduledFutureTask.super.run();
		//如果是週期性執行任務的話,需要重設下一次執行任務的時間
    else if (ScheduledFutureTask.super.runAndReset()) {
        setNextRunTime();
        reExecutePeriodic(outerTask);
    }
}
複製程式碼

從原始碼可以很明顯的看出,在重寫的 run 方法中會先if (!periodic)判斷當前任務是否是週期性任務,如果不是的話就直接呼叫run()方法;否則的話執行setNextRunTime()方法重設下一次任務執行的時間,並通過reExecutePeriodic(outerTask)方法將下一次待執行的任務放置到DelayedWorkQueue中。

因此,可以得出結論:ScheduledFutureTask最主要的功能是根據當前任務是否具有周期性,對非同步任務進行進一步封裝。如果不是週期性任務(呼叫 schedule 方法)則直接通過run()執行,若是週期性任務,則需要在每一次執行完後,重設下一次執行的時間,然後將下一次任務繼續放入到阻塞佇列中。

3. DelayedWorkQueue

在 ScheduledThreadPoolExecutor 中還有另外的一個重要的類就是 DelayedWorkQueue。為了實現其 ScheduledThreadPoolExecutor 能夠延時執行非同步任務以及能夠週期執行任務,DelayedWorkQueue 進行相應的封裝。DelayedWorkQueue 是一個基於堆的資料結構,類似於 DelayQueue 和 PriorityQueue。在執行定時任務的時候,每個任務的執行時間都不同,所以 DelayedWorkQueue 的工作就是按照執行時間的升序來排列,執行時間距離當前時間越近的任務在佇列的前面。

為什麼要使用 DelayedWorkQueue 呢?

定時任務執行時需要取出最近要執行的任務,所以任務在佇列中每次出隊時一定要是當前佇列中執行時間最靠前的,所以自然要使用優先順序佇列。

DelayedWorkQueue 是一個優先順序佇列,它可以保證每次出隊的任務都是當前佇列中執行時間最靠前的,由於它是基於堆結構的佇列,堆結構在執行插入和刪除操作時的最壞時間複雜度是 O(logN)。

DelayedWorkQueue 的資料結構

//初始大小
private static final int INITIAL_CAPACITY = 16;
//DelayedWorkQueue是由一個大小為16的陣列組成,陣列元素為實現RunnableScheduleFuture介面的類
//實際上為ScheduledFutureTask
private RunnableScheduledFuture<?>[] queue =
    new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
private final ReentrantLock lock = new ReentrantLock();
private int size = 0;
複製程式碼

可以看出 DelayedWorkQueue 底層是採用陣列構成的,關於DelayedWorkQueue 可以看這篇博主的文章,很詳細。

關於 DelayedWorkQueue 我們可以得出這樣的結論:DelayedWorkQueue 是基於堆的資料結構,按照時間順序將每個任務進行排序,將待執行時間越近的任務放在在佇列的隊頭位置,以便於最先進行執行

4.ScheduledThreadPoolExecutor 執行過程

現在我們對 ScheduledThreadPoolExecutor 的兩個內部類 ScheduledFutueTask 和 DelayedWorkQueue 進行了瞭解,實際上這也是執行緒池工作流程中最重要的兩個關鍵因素:任務以及阻塞佇列。現在我們來看下 ScheduledThreadPoolExecutor 提交一個任務後,整體的執行過程。以 ScheduledThreadPoolExecutor 的 schedule 方法為例,具體原始碼為:

public ScheduledFuture<?> schedule(Runnable command,
                                   long delay,
                                   TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
	//將提交的任務轉換成ScheduledFutureTask
    RunnableScheduledFuture<?> t = decorateTask(command,
        new ScheduledFutureTask<Void>(command, null,
                                      triggerTime(delay, unit)));
    //延時執行任務ScheduledFutureTask
	delayedExecute(t);
    return t;
}
複製程式碼

方法很容易理解,為了滿足 ScheduledThreadPoolExecutor 能夠延時執行任務和能週期執行任務的特性,會先將實現 Runnable 介面的類轉換成 ScheduledFutureTask。然後會呼叫delayedExecute方法進行執行任務,這個方法也是關鍵方法,來看下原始碼:

private void delayedExecute(RunnableScheduledFuture<?> task) {
    if (isShutdown())
		//如果當前執行緒池已經關閉,則拒絕任務
        reject(task);
    else {
		//將任務放入阻塞佇列中
        super.getQueue().add(task);
        if (isShutdown() &&
            !canRunInCurrentRunState(task.isPeriodic()) &&
            remove(task))
            task.cancel(false);
        else
			//保證至少有一個執行緒啟動,即使corePoolSize=0
            ensurePrestart();
    }
}
複製程式碼

delayedExecute方法的主要邏輯請看註釋,可以看出該方法的重要邏輯會是在ensurePrestart()方法中,它的原始碼為:

void ensurePrestart() {
    int wc = workerCountOf(ctl.get());
    if (wc < corePoolSize)
        addWorker(null, true);
    else if (wc == 0)
        addWorker(null, false);
}
複製程式碼

可以看出該方法邏輯很簡單,關鍵在於它所呼叫的addWorker方法,該方法主要功能:新建Worker類,當執行任務時,就會呼叫被Worker所重寫的run方法,進而會繼續執行runWorker方法。在runWorker方法中會呼叫getTask方法從阻塞佇列中不斷的去獲取任務進行執行,直到從阻塞佇列中獲取的任務為 null 的話,執行緒結束終止。addWorker 方法是 ThreadPoolExecutor 類中的方法,對 ThreadPoolExecutor 的原始碼分析可以看這篇文章,很詳細。

5.總結

  1. ScheduledThreadPoolExecutor 繼承了 ThreadPoolExecutor 類,因此,整體上功能一致,執行緒池主要負責建立執行緒(Worker 類),執行緒從阻塞佇列中不斷獲取新的非同步任務,直到阻塞佇列中已經沒有了非同步任務為止。但是相較於 ThreadPoolExecutor 來說,ScheduledThreadPoolExecutor 具有延時執行任務和可週期性執行任務的特性,ScheduledThreadPoolExecutor 重新設計了任務類ScheduleFutureTask,ScheduleFutureTask 重寫了run方法使其具有可延時執行和可週期性執行任務的特性。另外,阻塞佇列DelayedWorkQueue是可根據優先順序排序的佇列,採用了堆的底層資料結構,使得與當前時間相比,待執行時間越靠近的任務放置隊頭,以便執行緒能夠獲取到任務進行執行;

  2. 執行緒池無論是 ThreadPoolExecutor 還是 ScheduledThreadPoolExecutor,在設計時的三個關鍵要素是:任務,執行者以及任務結果。它們的設計思想也是完全將這三個關鍵要素進行了解耦。

    執行者

    任務的執行機制,完全交由Worker類,也就是進一步了封裝了 Thread。向執行緒池提交任務,無論為 ThreadPoolExecutor 的 execute 方法和 submit 方法,還是 ScheduledThreadPoolExecutor 的 schedule 方法,都是先將任務移入到阻塞佇列中,然後通過 addWork 方法新建了 Work 類,並通過 runWorker 方法啟動執行緒,並不斷的從阻塞對列中獲取非同步任務執行交給 Worker 執行,直至阻塞佇列中無法取到任務為止。

    任務

    在 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 中任務是指實現了 Runnable 介面和 Callable 介面的實現類。ThreadPoolExecutor 中會將任務轉換成FutureTask類,而在 ScheduledThreadPoolExecutor 中為了實現可延時執行任務和週期性執行任務的特性,任務會被轉換成ScheduledFutureTask類,該類繼承了 FutureTask,並重寫了 run 方法。

    任務結果

    在 ThreadPoolExecutor 中提交任務後,獲取任務結果可以通過 Future 介面的類,在 ThreadPoolExecutor 中實際上為 FutureTask 類,而在 ScheduledThreadPoolExecutor 中則是ScheduledFutureTask

相關文章