在最近的工做中,須要實現一個當一個任務執行完後,再等 100 毫秒而後再次執行的功能。當時最早反映到的就是 java 線程池的 ScheduledExecutorService,而 ScheduledExecutorService 有兩個週期性執行任務的方法,分別是 scheduleAtFixedRate 與 scheduleWithFixedDelay,當時對這兩個方法也不大瞭解,感受和個人理解有所誤差,因此對這兩個方法進行了研究。java
想要了解 scheduleWithFixedDelay 和 scheduleAtFixedRate 這兩個週期性執行任務的方法,首先要了解 ScheduledExecutorService 的原理。在《java 併發編程的藝術》一書中有詳細的解說,這裏就簡單的闡述一下。
ScheduledExecutorService 與其餘線程池的區別,主要在於在執行前將任務封裝爲ScheduledFutureTask與其使用的阻塞隊列DelayedWorkQueue。編程
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 是一個優先隊列,在元素出隊時,ScheduledFutureTask 的 time 最小的元素將優先出隊,若是 time 值相同則判斷 sequenceNumber,先入隊的元素先出隊。
而 DelayedWorkQueue 也是 ScheduledExecutorService 可以定時執行任務的核心類。
首先回顧一下線程池的執行流程:併發
當工做線程執行 DelayedWorkQueue 的出隊方法時,DelayedWorkQueue 首先獲取到 time 值最小的 ScheduledFutureTask,即將要最早執行的任務。而後用 time 值(任務要執行的時間戳)與當前時間做比較,判斷任務執行時間是否到期,若然到期,元素立馬出隊,交由工做線程執行。
可是當 time 值還沒到期呢?那麼 time 將會減去當前時間,獲得 delay 值(延遲多少時間後執行任務),而後使用方法Condition.awaitNanos(long nanosTimeout),阻塞獲取任務的工做線程,直到通過了 delay 時間,即到達了任務的執行時間,元素纔會出隊,交由工做線程執行。ide
根據我以前的理解,認爲 scheduleAtFixedRate 是絕對週期性執行,例如間隔週期爲 10 秒,那麼任務每隔 10 秒都會執行一次,無論任務是否成功執行。可是個人理解是錯誤的,這兩個方法的功能分別是:線程
要清楚,一個定時任務,無論是否爲週期性執行,都將會只由一條工做線程執行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 並不在行。