1.ScheduledThreadPoolExecutor
整體結構剖析。
1.1類圖介紹
根據上面類圖圖可以看到Executor其實是一個工具類,裡面提供了好多靜態方法,根據使用者選擇返回不同的執行緒池例項。可以看到ScheduledThreadPoolExecutor
繼承了 ThreadPoolExecutor
並實現 ScheduledExecutorService
介面。執行緒池佇列是 DelayedWorkQueue
,和 DelayedQueue
類似是一個延遲佇列。
ScheduledFutureTask
是具有返回值的任務,繼承自 FutureTask,FutureTask 內部有個變數 state 用來表示任務的狀態,一開始狀態為 NEW,所有狀態為:
private static final int NEW = 0;//初始狀態 private static final int COMPLETING = 1;//執行中狀態 private static final int NORMAL = 2;//正常執行結束狀態 private static final int EXCEPTIONAL = 3;//執行中異常 private static final int CANCELLED = 4;//任務被取消 private static final int INTERRUPTING = 5;//任務正在被中斷 private static final int INTERRUPTED = 6;//任務已經被中斷
FutureTask可能的任務狀態轉換路徑如下所示:
NEW -> COMPLETING -> NORMAL //初始狀態->執行中->正常結束 NEW -> COMPLETING -> EXCEPTIONAL//初始狀態->執行中->執行異常 NEW -> CANCELLED//初始狀態->任務取消 NEW -> INTERRUPTING -> INTERRUPTED//初始狀態->被中斷中->被中斷
其實ScheduledFutureTask
內部還有個變數 period 用來表示任務的型別,其任務型別如下:
-
period=0,說明當前任務是一次性的,執行完畢後就退出了。
-
period 為負數,說明當前任務為 fixed-delay 任務,是定時可重複執行任務。
-
period 為整數,說明當前任務為 fixed-rate 任務,是定時可重複執行任務。
接下來我們可以看到ScheduledThreadPoolExecutor
的造函式如下
//使用改造後的Delayqueue. public ScheduledThreadPoolExecutor(int corePoolSize) { //呼叫父類ThreadPoolExecutor的建構函式 super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS, new DelayedWorkQueue()); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); }
根據上面程式碼可以看到執行緒池佇列是 DelayedWorkQueue
2、原理分析
我們主要看三個重要的函式,如下所示:
schedule(Runnable command, long delay,TimeUnit unit) scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit) scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)
2.1、schedule(Runnable command, long delay,TimeUnit unit)
方法
該方法作用是提交一個延遲執行的任務,任務從提交時間算起延遲 unit 單位的 delay 時間後開始執行,提交的任務不是週期性任務,任務只會執行一次,程式碼如下:
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) { //(1)引數校驗 if (command == null || unit == null) throw new NullPointerException(); //(2)任務轉換 RunnableScheduledFuture<?> t = decorateTask(command, new ScheduledFutureTask<Void>(command, null, triggerTime(delay, unit))); //(3)新增任務到延遲佇列 delayedExecute(t); return t; }
可以看到上面程式碼所示,程式碼(1)引數校驗,如果 command 或者 unit 為 null,丟擲 NPE 異常。
程式碼(2)裝飾任務,把提交的 command 任務轉換為 ScheduledFutureTask
,ScheduledFutureTask
是具體放入到延遲佇列裡面的東西,由於是延遲任務,所以 ScheduledFutureTask
實現了 long getDelay(TimeUnit unit)
和 int compareTo(Delayed other)
方法,triggerTime 方法轉換延遲時間為絕對時間,也就是把當前時間的納秒數加上延遲的納秒數後的 long 型值。
接下來我們需要看 ScheduledFutureTask
的建構函式,如下所示:
ScheduledFutureTask(Runnable r, V result, long ns) { //呼叫父類FutureTask的建構函式 super(r, result); this.time = ns; this.period = 0;//period為0,說明為一次性任務 this.sequenceNumber = sequencer.getAndIncrement(); }
根據建構函式可以看到內部首先呼叫了父類 FutureTask 的建構函式,父類 FutureTask 的建構函式程式碼如下:
//通過介面卡把runnable轉換為callable public FutureTask(Runnable runnable, V result) { this.callable = Executors.callable(runnable, result); this.state = NEW; //設定當前任務狀態為NEW }
根據上面程式碼可以看到FutureTask 中任務又被轉換為了 Callable 型別後,儲存到了變數 this.callable 裡面,並設定 FutureTask 的任務狀態為 NEW。
然後 ScheduledFutureTask
建構函式內部設定 time 為上面說的絕對時間,需要注意這裡 period 的值為 0,這說明當前任務為一次性任務,不是定時反覆執行任務。
其中 long getDelay(TimeUnit unit)
方法程式碼如下,用來獲取當前任務還有多少時間就過期了,程式碼如下所示:
//元素過期演算法,裝飾後時間-當前時間,就是即將過期剩餘時間 public long getDelay(TimeUnit unit) { return unit.convert(time - now(), NANOSECONDS); }
接下來接著看compareTo(Delayed other)
方法,程式碼如下:
public int compareTo(Delayed other) { if (other == this) // compare zero ONLY if same object return 0; if (other instanceof ScheduledFutureTask) { ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other; long diff = time - x.time; if (diff < 0) return -1; else if (diff > 0) return 1; else if (sequenceNumber < x.sequenceNumber) return -1; else return 1; } long d = (getDelay(TimeUnit.NANOSECONDS) - other.getDelay(TimeUnit.NANOSECONDS)); return (d == 0) ? 0 : ((d < 0) ? -1 : 1); }
根據上面程式碼的執行邏輯,可以看到compareTo 作用是加入元素到延遲佇列後,內部建立或者調整堆時候會使用該元素的 compareTo 方法與佇列裡面其他元素進行比較,讓最快要過期的元素放到隊首。所以無論什麼時候向佇列裡面新增元素,隊首的的元素都是最即將過期的元素。
接下來接著看程式碼(3)新增任務到延遲佇列,delayedExecute 的程式碼如下:
private void delayedExecute(RunnableScheduledFuture<?> task) { //(4)如果執行緒池關閉了,則執行執行緒池拒絕策略 if (isShutdown()) reject(task); else { //(5)新增任務到延遲佇列 super.getQueue().add(task); //(6)再次檢查執行緒池狀態 if (isShutdown() && !canRunInCurrentRunState(task.isPeriodic()) && remove(task)) task.cancel(false); else //(7)確保至少一個執行緒在處理任務 ensurePrestart(); } }
可以看到程式碼(4)首先判斷當前執行緒池是否已經關閉了,如果已經關閉則執行執行緒池的拒絕策略(如果不知道執行緒池的拒絕策略可以看前一篇執行緒池的介紹。)
否者執行程式碼(5)新增任務到延遲佇列。新增完畢後還要重新檢查執行緒池是否被關閉了,如果已經關閉則從延遲佇列裡面刪除剛才新增的任務,但是有可能執行緒池執行緒已經從任務佇列裡面移除了該任務,也就是該任務已經在執行了,所以還需要呼叫任務的 cancle 方法取消任務。
如果程式碼(6)判斷結果為 false,則會執行程式碼(7)確保至少有一個執行緒在處理任務,即使核心執行緒數 corePoolSize 被設定為 0.
ensurePrestart 程式碼如下:
void ensurePrestart() { int wc = workerCountOf(ctl.get()); //增加核心執行緒數 if (wc < corePoolSize) addWorker(null, true); //如果初始化corePoolSize==0,則也新增一個執行緒。 else if (wc == 0) addWorker(null, false); } }
如上程式碼首先首先獲取執行緒池中執行緒個數,如果執行緒個數小於核心執行緒數則新增一個執行緒,否者如果當前執行緒數為 0 則新增一個執行緒。
通過上面程式碼我們分析瞭如何新增任務到延遲佇列,下面我們看執行緒池裡面的執行緒如何獲取並執行任務的,從前面講解的 ThreadPoolExecutor
我們知道具體執行任務的執行緒是 Worker 執行緒,Worker 執行緒裡面呼叫具體任務的 run 方法進行執行,由於這裡任務是 ScheduledFutureTask
,所以我們下面看看 ScheduledFutureTask
的 run 方法。程式碼如下:
public void run() { //(8)是否只執行一次 boolean periodic = isPeriodic(); //(9)取消任務 if (!canRunInCurrentRunState(periodic)) cancel(false); //(10)只執行一次,呼叫schdule時候 else if (!periodic) ScheduledFutureTask.super.run(); //(11)定時執行 else if (ScheduledFutureTask.super.runAndReset()) { //(11.1)設定time=time+period setNextRunTime(); //(11.2)重新加入該任務到delay佇列 reExecutePeriodic(outerTask); } }
可以看到程式碼(8)isPeriodic 的作用是判斷當前任務是一次性任務還是可重複執行的任務,isPeriodic 的程式碼如下:
public boolean isPeriodic() { return period != 0; }
可知內部是通過 period 的值來判斷,由於轉換任務建立 ScheduledFutureTask 時候傳遞的 period 為 0 ,所以這裡 isPeriodic 返回 false。
程式碼(9)判斷當前任務是否應該被取消,canRunInCurrentRunState 的程式碼如下:
boolean canRunInCurrentRunState(boolean periodic) { return isRunningOrShutdown(periodic ? continueExistingPeriodicTasksAfterShutdown : executeExistingDelayedTasksAfterShutdown); }
這裡傳遞的 periodic 為 false,所以 isRunningOrShutdown
的引數為 executeExistingDelayedTasksAfterShutdown
,executeExistingDelayedTasksAfterShutdown
預設是 true 標示當其它執行緒呼叫了 shutdown 命令關閉了執行緒池後,當前任務還是要執行,否者如果為 false,標示當前任務要被取消。
由於 periodic 為 false,所以執行程式碼(10)呼叫父類 FutureTask 的 run 方法具體執行任務,FutureTask 的 run 方法程式碼如下:
public void run() { //(12) if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) return; //(13) try { Callable<V> c = callable; if (c != null && state == NEW) { V result; boolean ran; try { result = c.call(); ran = true; } catch (Throwable ex) { result = null; ran = false; //(13.1) setException(ex); } //(13.2) if (ran) set(result); } } finally { ...省略 } }
可以看到程式碼(12)如果任務狀態不是 NEW 則直接返回,或者如果當前任務狀態為NEW但是使用 CAS 設定當然任務的持有者為當前執行緒失敗則直接返回。程式碼(13)具體呼叫 callable 的 call 方法執行任務,這裡在呼叫前又判斷了任務的狀態是否為 NEW 是為了避免在執行程式碼(12)後其他執行緒修改了任務的狀態(比如取消了該任務)。
如果任務執行成功則執行程式碼(13.2)修改任務狀態,set 方法程式碼如下:
protected void set(V v) { //如果當前任務狀態為NEW,則設定為COMPLETING if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { outcome = v; //設定當前任務終狀為NORMAL,也就是任務正常結束 UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state finishCompletion(); } }
如上程式碼首先 CAS 設定當前任務狀態從 NEW 轉換到 COMPLETING,這裡多個執行緒呼叫時候只有一個執行緒會成功,成功的執行緒在通過 UNSAFE.putOrderedInt
設定任務的狀態為正常結束狀態,這裡沒有用 CAS 是因為同一個任務只可能有一個執行緒可以執行到這裡,這裡使用 putOrderedInt
比使用 CAS 函式或者 putLongVolatile
效率要高,並且這裡的場景不要求其它執行緒馬上對設定的狀態值可見。
這裡思考個問題,這裡什麼時候多個執行緒會同時執行 CAS 設定任務狀態從態從 NEW 到 COMPLETING?其實當同一個 comand 被多次提交到執行緒池時候就會存在這樣的情況,由於同一個任務共享一個狀態值 state。
如果任務執行失敗,則執行程式碼(13.1),setException 的程式碼如下,可見與 set 函式類似,程式碼如下:
protected void setException(Throwable t) { //如果當前任務狀態為NEW,則設定為COMPLETING if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { outcome = t; //設定當前任務終態為EXCEPTIONAL,也就是任務非正常結束 UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL);
finishCompletion(); } }
到這裡程式碼(10)邏輯執行完畢,一次性任務也就執行完畢了,
下面會講到如果任務是可重複執行的,則不會執行步驟(10)而是執行程式碼(11)。
2.2 scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit)方法
當任務執行完畢後,延遲固定間隔時間後再次執行(fixed-delay 任務):其中 initialDelay 說明提交任務後延遲多少時間開始執行任務 command,delay 表示當任務執行完畢後延長多少時間後再次執行 command 任務,unit 是 initialDelay 和 delay 的時間單位。任務會一直重複執行直到任務執行時候丟擲了異常或者取消了任務,或者關閉了執行緒池。scheduleWithFixedDelay
的程式碼如下:
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { //(14)引數校驗 if (command == null || unit == null) throw new NullPointerException(); if (delay <= 0) throw new IllegalArgumentException(); //(15)任務轉換,注意這裡是period=-delay<0 ScheduledFutureTask<Void> sft = new ScheduledFutureTask<Void>(command, null, triggerTime(initialDelay, unit), unit.toNanos(-delay)); RunnableScheduledFuture<Void> t = decorateTask(command, sft); sft.outerTask = t; //(16)新增任務到佇列 delayedExecute(t); return t;
}
如上程式碼(14)進行引數校驗,校驗失敗則丟擲異常,程式碼(15)轉換 command 任務為 ScheduledFutureTask
,這裡需要注意的是這裡傳遞給 ScheduledFutureTask
的 period 變數的值為 -delay,period < 0 這個說明該任務為可重複執行的任務。然後程式碼(16)新增任務到延遲佇列後返回。
任務新增到延遲佇列後執行緒池執行緒會從佇列裡面獲取任務,然後呼叫 ScheduledFutureTask
的 run 方法執行,由於這裡 period<0 所以 isPeriodic 返回 true,所以執行程式碼(11),runAndReset 的程式碼如下:
protected boolean runAndReset() { //(17) if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) return false; //(18) boolean ran = false; int s = state; try { Callable<V> c = callable; if (c != null && s == NEW) { try { c.call(); // don't set result ran = true; } catch (Throwable ex) { setException(ex); } } } finally { ... } return ran && s == NEW;//(19) }
該程式碼和 FutureTask 的 run 類似,只是任務正常執行完畢後不會設定任務的狀態,這樣做是為了讓任務成為可重複執行的任務,這裡多了程式碼(19)如果當前任務正常執行完畢並且任務狀態為 NEW 則返回 true 否者返回 false。
如果返回了 true 則執行程式碼(11.1)setNextRunTime
方法設定該任務下一次的執行時間,setNextRunTime
的程式碼如下:
private void setNextRunTime() { long p = period; if (p > 0)//fixed-rate型別任務 time += p; else//fixed-delay型別任務 time = triggerTime(-p); }
如上程式碼這裡 p < 0 說明當前任務為 fixed-delay
型別任務,然後設定 time 為當前時間加上 -p
的時間,也就是延遲 -p
時間後在次執行。
總結:本節介紹的 fixed-delay
型別的任務的執行實現原理如下,當新增一個任務到延遲佇列後,等 initialDelay 時間後,任務就會過期,過期的任務就會被從佇列移除,並執行,執行完畢後,會重新設定任務的延遲時間,然後在把任務放入延遲佇列實現的,依次往復。需要注意的是如果一個任務在執行某一個次時候丟擲了異常,那麼這個任務就結束了,但是不影響其它任務的執行。
2.3、scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)方法
相對起始時間點固定頻率呼叫指定的任務(fixed-rate 任務):當提交任務到執行緒池後延遲 initialDelay 個時間單位為 unit 的時間後開始執行任務 comand ,然後 initialDelay + period
時間點再次執行,然後在 initialDelay + 2 * period
時間點再次執行,依次往復,直到丟擲異常或者呼叫了任務的 cancel 方法取消了任務在結束或者關閉了執行緒池。
scheduleAtFixedRate
的原理與 scheduleWithFixedDelay
類似,下面我們講下不同點,首先呼叫 scheduleAtFixedRate
時候程式碼如下:
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { ... //裝飾任務類,注意period=period>0,不是負的 ScheduledFutureTask<Void> sft = new ScheduledFutureTask<Void>(command, null, triggerTime(initialDelay, unit), unit.toNanos(period)); ... return t; }
如上程式碼 fixed-rate
型別的任務在轉換 command
任務為 ScheduledFutureTask
的時候設定的 period=period
不在是 -period
。
所以當前任務執行完畢後,呼叫 setNextRunTime
設定任務下次執行的時間時候執行的是 time += p
而不在是 time = triggerTime(-p);
。
總結:相對於 fixed-delay
任務來說,fixed-rate
方式執行規則為時間為 initdelday + n*period;
時候啟動任務,但是如果當前任務還沒有執行完,下一次要執行任務的時間到了,不會併發執行,下次要執行的任務會延遲執行,要等到當前任務執行完畢後在執行一個任務。
3、總結
ScheduledThreadPoolExecutor
的實現原理,其內部使用的 DelayQueue
來存放具體任務,其中任務分為三種,其中一次性執行任務執行完畢就結束了,fixed-delay
任務保證同一個任務多次執行之間間隔固定時間,fixed-rate
任務保證任務執行按照固定的頻率執行,其中任務型別使用 period
的值來區分。