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