ScheduledExecutorService中scheduleAtFixedRate方法與scheduleWithFixedDelay方法的區別
-
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這兩個方法看起來就不是那麼好區分了,今天就帶大家從原始碼角度看看這兩個方法的區別.
-
我們先看看這兩個方法的區別
下面是這兩個方法的原始碼
從上面的圖上可以看到唯一的不同就是在建立ScheduledFutureTask物件的時候,scheduleWithFixedDelay將我們傳入的delay取了個負數。所以這兩個方法的區別都會在ScheduledFutureTask這個類中。
先說下ScheduledFutureTask這個類吧,它是ScheduledThreadPoolExecutor的內部類,我們看下它的繼承關係
從上圖中能看到ScheduledFutureTask,間接繼承了Runnable介面,會實現run方法。而我們ScheduledThreadPoolExecutor類中真正執行任務的類其實也就是呼叫ScheduledFutureTask的run方法。也間接實現了Comparable介面的比較方法。
-
下面以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); }
-
結論
scheduleAtFixedRate或者scheduleWithFixedDelay對於從第2次開始的任務的計算時間不一樣:
- scheduleAtFixedRate 下次任務的時間=(開始新增任務時計算出來的首次任務執行時間+(任務執行次數-1)*period)
- scheduleWithFixedDelay 下次任務的時間=當前任務結束時間+period
需要注意的是,下次任務時間都只是計算出來的理論值,如果任務的執行時間大於週期任務的period,或者設定的執行緒池中執行緒太少,就會出現下次任務執行時間<時間任務執行時間