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

wang03發表於2021-08-29

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,或者設定的執行緒池中執行緒太少,就會出現下次任務執行時間<時間任務執行時間

相關文章