詳解ScheduledExecutorService的週期性執行方法

詳解 ScheduledExecutorService 的週期性執行方法

在最近的工做中,須要實現一個當一個任務執行完後,再等 100 毫秒而後再次執行的功能。當時最早反映到的就是 java 線程池的 ScheduledExecutorService,而 ScheduledExecutorService 有兩個週期性執行任務的方法,分別是 scheduleAtFixedRate 與 scheduleWithFixedDelay,當時對這兩個方法也不大瞭解,感受和個人理解有所誤差,因此對這兩個方法進行了研究。java

ScheduledExecutorService 的基本原理

想要了解 scheduleWithFixedDelay 和 scheduleAtFixedRate 這兩個週期性執行任務的方法,首先要了解 ScheduledExecutorService 的原理。在《java 併發編程的藝術》一書中有詳細的解說,這裏就簡單的闡述一下。
ScheduledExecutorService 與其餘線程池的區別,主要在於在執行前將任務封裝爲ScheduledFutureTask與其使用的阻塞隊列DelayedWorkQueue編程

ScheduledFutureTask

private class ScheduledFutureTask<V>
            extends FutureTask<V> implements RunnableScheduledFuture<V> {

        /** 表示這個任務添加到ScheduledExecutorService中的序號 */
        private final long sequenceNumber;

        /** T表示這個任務將要被執行的具體時間(時間戳) */
        private long time;

        /**
         * 表示任務執行的間隔週期,若爲0則表示不是週期性執行任務
         */
        private final long period;

        /*省略如下代碼*/

    }

DelayedWorkQueue

DelayedWorkQueue 是一個優先隊列,在元素出隊時,ScheduledFutureTask 的 time 最小的元素將優先出隊,若是 time 值相同則判斷 sequenceNumber,先入隊的元素先出隊。
而 DelayedWorkQueue 也是 ScheduledExecutorService 可以定時執行任務的核心類。
首先回顧一下線程池的執行流程:併發

  1. 向線程池提交任務,這時任務將入隊到該線程池的阻塞隊列
  2. 工做線程不斷從隊列中取出任務,並執行,若然隊列中沒有任務,工做線程將阻塞直到任務的到來。

當工做線程執行 DelayedWorkQueue 的出隊方法時,DelayedWorkQueue 首先獲取到 time 值最小的 ScheduledFutureTask,即將要最早執行的任務。而後用 time 值(任務要執行的時間戳)與當前時間做比較,判斷任務執行時間是否到期,若然到期,元素立馬出隊,交由工做線程執行。
可是當 time 值還沒到期呢?那麼 time 將會減去當前時間,獲得 delay 值(延遲多少時間後執行任務),而後使用方法Condition.awaitNanos(long nanosTimeout),阻塞獲取任務的工做線程,直到通過了 delay 時間,即到達了任務的執行時間,元素纔會出隊,交由工做線程執行。ide

scheduleAtFixedRate 與 scheduleWithFixedDelay

根據我以前的理解,認爲 scheduleAtFixedRate 是絕對週期性執行,例如間隔週期爲 10 秒,那麼任務每隔 10 秒都會執行一次,無論任務是否成功執行。可是個人理解是錯誤的,這兩個方法的功能分別是:線程

  1. scheduleAtFixedRate:任務執行完成後,在提交任務到任務執行完成後的時間是否通過了 period,若然通過了,即立刻再次執行該任務。不然等待,直到提交任務到如今已經通過了 period 時間,再次執行該任務。
  2. scheduleWithFixedDelay:任務執行完成後,等待 delay 時間,而後再次執行。

要清楚,一個定時任務,無論是否爲週期性執行,都將會只由一條工做線程執行code

首先看下這兩個方法的源碼對象

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                long initialDelay,
                                                long period,
                                                TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (period <= 0)
        throw new IllegalArgumentException();
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                        null,
                                        triggerTime(initialDelay, unit),
                                        unit.toNanos(period));
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
    delayedExecute(t);
    return t;
}

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                    long initialDelay,
                                                    long delay,
                                                    TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (delay <= 0)
        throw new IllegalArgumentException();
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                        null,
                                        triggerTime(initialDelay, unit),
                                        unit.toNanos(-delay));
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
    delayedExecute(t);
    return t;
}

其實兩個方法沒有太大區別,只是在構建 ScheduledFutureTask 的時候,ScheduledFutureTask 的 period 屬性有正負差異,scheduleAtFixedRate 方法構建 ScheduledFutureTask 的 period 爲負數,而 scheduleWithFixedDelay 爲正數。
接下來查看 ScheduledFutureTask 的 run 方法,工做線程在執行任務時將會調用該方法隊列

/**
 * Overrides FutureTask version so as to reset/requeue if periodic.
 */
public void run() {
    boolean periodic = isPeriodic();
    if (!canRunInCurrentRunState(periodic))
        cancel(false);//1
    else if (!periodic)
        ScheduledFutureTask.super.run();//2
    else if (ScheduledFutureTask.super.runAndReset()) {
        //3
        setNextRunTime();
        reExecutePeriodic(outerTask);
    }
}

若是定時任務時週期性執行方法,將會進入到 3 的執行邏輯,固然在這以前將會調用 runAndReset 執行任務邏輯。
當任務邏輯執行完成後,將會調用 setNextRunTime。源碼

/**
 * Sets the next time to run for a periodic task.
 */
private void setNextRunTime() {
    long p = period;
    if (p > 0)
        time += p;
    else
        time = triggerTime(-p);
}

long triggerTime(long delay) {
    return now() +
        ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}

若是 period 爲正數數,即執行的方法爲 scheduleAtFixedRate,在任務的執行時間上添加 period 時間。
而 period 爲負數,即執行的方法爲 scheduleWithFixedDelay,將 time 改寫爲當前時間加上 period 時間。
執行完 setNextRunTime 方法後,將執行 reExecutePeriodic 方法,即從新將該 ScheduledFutureTask 對象,從新添加到隊列中,等待下一次執行。
要清楚,不論調用哪一個週期性執行方法,都是須要等到任務邏輯執行完成後,才能再次添加到隊列中,等待下一次執行。it

scheduleAtFixedRate 方法,每次都是在 time 的基礎上添加 period 時間,若是任務邏輯的執行時間大於 period,那麼在定時任務再次出隊前,time 一定是小於當前時間,立刻出隊被工做線程執行。由於 time 每次都是任務開始執行的時間點。
scheduleWithFixedDelay 方法,每次都將 time 設置爲當前時間加上 period,那麼輪到定時任務再次出隊時,一定是通過了 period 時間,才能被工做線程執行。

總結

對於 ScheduledExecutorService 必定要清楚,週期性執行任務,必定是等到上一次執行完成後,才能再次執行,即每一個任務只由一條線程執行。那麼要實現當達到必定時候後,不論任務是否執行完成,都將再次執行任務的功能,ScheduledExecutorService 的兩個週期性執行方法都是不能實現的。其實也就是對於複雜的時間調度控制,ScheduledExecutorService 並不在行。

相關文章
相關標籤/搜索