ScheduledExecutorService中scheduleAtFixedRate方法與scheduleWithFixedDelay方法的區別

  1. ScheduledThreadPoolExecutor繼承自ThreadPoolExecutor,可以作為執行緒池來使用,同時實現了ScheduledExecutorService介面,來執行一些週期性的任務。ScheduledExecutorService一般常用的方法主要就4個

        public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)
    public <V> ScheduledFuture<V> schedule(Callable<V> callable,long delay, TimeUnit unit);
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit);
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay, long delay,TimeUnit unit);

    兩個schedule方法都很明確,就是執行一次Runnable、Callable任務。

    scheduleAtFixedRate和scheduleWithFixedDelay這兩個方法看起來就不是那麼好區分了,今天就帶大家從原始碼角度看看這兩個方法的區別.

  2. 我們先看看這兩個方法的區別

    下面是這兩個方法的原始碼

​ 從上面的圖上可以看到唯一的不同就是在建立ScheduledFutureTask物件的時候,scheduleWithFixedDelay將我們傳入的delay取了個負數。所以這兩個方法的區別都會在ScheduledFutureTask這個類中。

​ 先說下ScheduledFutureTask這個類吧,它是ScheduledThreadPoolExecutor的內部類,我們看下它的繼承關係

從上圖中能看到ScheduledFutureTask,間接繼承了Runnable介面,會實現run方法。而我們ScheduledThreadPoolExecutor類中真正執行任務的類其實也就是呼叫ScheduledFutureTask的run方法。也間接實現了Comparable介面的比較方法。

  1. 下面以scheduleAtFixedRate看看內部呼叫邏輯

        public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
    long initialDelay,
    long period,
    TimeUnit unit) {
    if (command == null || unit == null)
    throw new NullPointerException();
    if (period <= 0L)
    throw new IllegalArgumentException();
    ScheduledFutureTask<Void> sft =
    new ScheduledFutureTask<Void>(command,
    null,
    //這裡是計算首次執行實現
    triggerTime(initialDelay, unit),
    unit.toNanos(period),
    sequencer.getAndIncrement());
    //這裡當前是直接返回的上面的sft,這個是留給子類去擴充套件的
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
    //所以這裡也就是把上面建立的ScheduledFutureTask加到執行緒池的任務佇列中去
    delayedExecute(t);
    return t;
    }

    這裡是ScheduledFutureTask的構造方法

            ScheduledFutureTask(Runnable r, V result, long triggerTime,
    long period, long sequenceNumber) {
    super(r, result);
    //這個是任務首次執行的時間
    this.time = triggerTime;
    //這裡的程式碼很簡單,只是將它賦值給了成員變數period。
    //其中scheduleWithFixedDelay是在外面取了個負數傳了進來,scheduleAtFixedRate則是原樣傳了進來
    this.period = period;
    //這個是AtomicLong型別的,每次都+1,對我們加入的任務做了個編號
    this.sequenceNumber = sequenceNumber;
    }

    我們再看看delayedExecute(t);的內部

        private void delayedExecute(RunnableScheduledFuture<?> task) {
    if (isShutdown())
    reject(task);
    else {
    //重點在這裡,這裡會把上面的ScheduledFutureTask加入到執行緒池的任務佇列中,
    //這裡的super.getQueue()這個佇列,是在ScheduledThreadPoolExecutor構造方法中定義的,也是ScheduledThreadPoolExecutord的內部類,類名是DelayedWorkQueue
    //DelayedWorkQueue其實是一個最小堆,會對加入它的元素,呼叫compareTo方法進行排序,首個元素是最小的
    //對應當前這裡,就是呼叫ScheduledFutureTask的compareTo方法進行排序,也就是佇列中的任務是按照執行時間的先後順序排序的
    //最終執行緒池執行任務的時候從首部依次獲取task,具體獲取任務的時候,DelayedWorkQueue會首先獲取任務,檢視對應的執行時間,如果任務時間沒有到,就會呼叫Condition.awaitNanos去暫停,直到到達執行時間或者通過給佇列中新增任務呼叫Condition.signal去喚醒
    super.getQueue().add(task);
    if (!canRunInCurrentRunState(task) && remove(task))
    task.cancel(false);
    else
    //這裡是根據執行緒池當前的執行緒數,如果小於核心執行緒數,就會新啟動執行緒去執行任務
    ensurePrestart();
    }
    }

    上面已經可以看到把任務已經加到執行緒池中去了,後面就是具體由執行緒池去執行任務了,所以我們直接去ScheduledFutureTask檢視run方法就可以了

            public void run() {
    if (!canRunInCurrentRunState(this))
    cancel(false);
    else if (!isPeriodic())
    super.run();
    //具體在這裡會呼叫我們傳入的run方法
    else if (super.runAndReset()) {
    //在這裡會更新成員變數time,scheduleAtFixedRate和scheduleWithFixedDelay的區別也全在這裡了,下面我們去看看這裡
    setNextRunTime();
    //這裡會重新將outerTask加入到執行緒池的任務佇列中,這裡的outerTask==我們當前執行run方法的物件this
    reExecutePeriodic(outerTask);
    }
    }
    }

    通過上面的程式碼也能看到,我們的run方法是不會同時由多次執行的,舉個例子,如果我們呼叫scheduleAtFixedRate或者scheduleWithFixedDelay方法,傳入的Runnable的物件,需要執行10s,而我們設定的週期是2s,是不會在第一次Runnable的10s的週期任務啟動後2s,就啟動第2次週期任務的。它只會在第一個Runnable的10s的週期任務結束後,重新加入到任務佇列中之後,才會啟動下次的任務。

        private void setNextRunTime() {
    //這裡的period ,scheduleAtFixedRate傳入的是正數,scheduleWithFixedDelay傳入的是負數
    long p = period;
    if (p > 0)
    //所以scheduleAtFixedRate會走這裡,這裡的time開始時時首次任務的開始執行時間,所以下次任務的時間就是(開始新增任務時計算出來的首次任務執行時間(這個時間不一定是任務首次執行的真正時間)+(任務執行次數-1)*period)
    time += p;
    else
    //這裡對p取負,就會還原成正數,也就是我們最初呼叫scheduleWithFixedDelay時傳入的值,這裡的下次執行時間會用當前系統時間(可以看成當前Runnable執行的結束時間)+period來設定
    time = triggerTime(-p);
    }
  2. 結論

    scheduleAtFixedRate或者scheduleWithFixedDelay對於從第2次開始的任務的計算時間不一樣:

    • scheduleAtFixedRate 下次任務的時間=(開始新增任務時計算出來的首次任務執行時間+(任務執行次數-1)*period
    • scheduleWithFixedDelay 下次任務的時間=當前任務結束時間+period

    需要注意的是,下次任務時間都只是計算出來的理論值,如果任務的執行時間大於週期任務的period,或者設定的執行緒池中執行緒太少,就會出現下次任務執行時間<時間任務執行時間