執行緒池之ScheduledThreadPoolExecutor

你聽___發表於2018-05-06

執行緒池之ScheduledThreadPoolExecutor

1. ScheduledThreadPoolExecutor簡介

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

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

相關文章