前言
在上一篇線程池的文章《Java線程池原理分析ThreadPoolExecutor篇》中從ThreadPoolExecutor源碼分析了其運行機制。限于篇幅,留下了ScheduledThreadPoolExecutor未做分析,因此本文繼續從源代碼出發分析ScheduledThreadPoolExecutor的內部原理。
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor同ThreadPoolExecutor一樣也可以從 Executors線程池工廠創建,所不同的是它具有定時執行,以周期或間隔循環執行任務等功能。
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService {
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
}
public interface ScheduledExecutorService extends ExecutorService {
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
public ScheduledFuture<?> schedule(Runnable command,
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);
}
ScheduledThreadPoolExecutor繼承自ThreadPoolExecutor,因此它具有ThreadPoolExecutor的所有能力。
通過super方法的參數可知,核心線程的數量即傳入的參數,而線程池的線程數為Integer.MAX_VALUE,幾乎為無上限。
這里采用了DelayedWorkQueue任務隊列,也是定時任務的核心,留在后面分析。
ScheduledThreadPoolExecutor實現了ScheduledExecutorService 中的接口:
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
延時執行Callable任務的功能。
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
延時執行Runnable任務的功能。
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
可以延時循環執行周期性任務。
假設任務執行時間固定2s,period為1s,因為任務的執行時間大于規定的period,所以任務會每隔2s(任務執行時間)開始執行一次。如果任務執行時間固定為0.5s,period為1s,因為任務執行時間小于period,所以任務會每隔1s(period)開始執行一次。實際任務的執行時間即可能是大于period的,也可能小于period,scheduleAtFixedRate的好處就是每次任務的開始時間間隔必然大于等于period。
假設一項業務需求每天凌晨3點將數據庫備份,然而數據庫備份的時間小于24H,最適合用scheduleAtFixedRate方法實現。
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
可以延時以相同間隔時間循環執行任務。
假設任務執行的時間固定為2s,delay為1s,那么任務會每隔3s(任務時間+delay)開始執行一次。
如果業務需求本次任務的結束時間與下一個任務的開始時間固定,使用scheduleWithFixedDelay可以方便地實現業務。
ScheduledFuture
四個執行任務的方法都返回了ScheduledFuture對象,它與Future有什么區別?
public interface ScheduledFuture<V> extends Delayed, Future<V> {
}
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
public interface Comparable<T> {
public int compareTo(T o);
}
可以看到ScheduledFuture也繼承了Future,并且繼承了Delayed,增加了getDelay方法,而Delayed繼承自Comparable,所以具有compareTo方法。
四種執行定時任務的方法
schedule(Runnable command,long delay, TimeUnit unit)
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
RunnableScheduledFuture<?> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null,
triggerTime(delay, unit)));
delayedExecute(t);
return t;
}
這個方法中出現了幾個陌生的類,首先是ScheduledFutureTask:
private class ScheduledFutureTask<V> extends FutureTask<V> implements RunnableScheduledFuture<V> {
...
ScheduledFutureTask(Runnable r, V result, long ns) {
super(r, result);
this.time = ns;
this.period = 0;
this.sequenceNumber = sequencer.getAndIncrement();
}
}
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
public interface RunnableScheduledFuture<V> extends RunnableFuture<V>, ScheduledFuture<V> {
boolean isPeriodic();
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
這個類是ScheduledThreadPoolExecutor的內部類,繼承自FutureTask實現了RunnableScheduledFuture接口。RunnableScheduledFuture有些復雜,繼承自RunnableFuture和ScheduledFuture接口。可見ScheduledThreadPoolExecutor身兼多職。這個類既可以作為Runnable被線程執行,又可以作為FutureTask用于獲取Callable任務call方法返回的結果。
在FutureTask的構造方法中傳入Runnable對象會將其轉換為返回值為null的Callable對象。
/**
* Modifies or replaces the task used to execute a runnable.
* This method can be used to override the concrete
* class used for managing internal tasks.
* The default implementation simply returns the given task.
*
* @param runnable the submitted Runnable
* @param task the task created to execute the runnable
* @param <V> the type of the task's result
* @return a task that can execute the runnable
* @since 1.6
*/
protected <V> RunnableScheduledFuture<V> decorateTask(
Runnable runnable, RunnableScheduledFuture<V> task) {
return task;
}
從decorateTask的字面意義判斷它將具體的RunnableScheduledFuture實現類向上轉型為RunnableScheduledFuture接口。從它的方法描述和實現看出它只是簡單的將ScheduledFutureTask向上轉型為RunnableScheduledFuture接口,由protected 修飾符可知設計者希望子類擴展這個方法的實現。
之所以向上轉型為RunnableScheduledFuture接口,設計者也是希望將具體與接口分離。
private void delayedExecute(RunnableScheduledFuture<?> task) {
if (isShutdown())
//情況一
reject(task);
else {
super.getQueue().add(task);
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
//情況二
task.cancel(false);
else
//情況三
ensurePrestart();
}
}
boolean canRunInCurrentRunState(boolean periodic) {
return isRunningOrShutdown(periodic ?
continueExistingPeriodicTasksAfterShutdown :
executeExistingDelayedTasksAfterShutdown);
}
private volatile boolean executeExistingDelayedTasksAfterShutdown = true;
final boolean isRunningOrShutdown(boolean shutdownOK) {
int rs = runStateOf(ctl.get());
return rs == RUNNING || (rs == SHUTDOWN && shutdownOK);
}
delayedExecute方法負責執行延時任務。
情況一 : 先判斷線程池是否關閉,若關閉則拒絕任務。
情況二:線程池未關閉,將任務添加到父類的任務隊列,即DelayedWorkQueue中。下面再次判斷線程池是否關閉,并且判斷canRunInCurrentRunState方法的返回值是否為false。因為傳入Runnable參數,task.isPeriodic()為false,所以isRunningOrShutdown返回true。所以這里不會執行到。
情況三:任務成功添加到任務隊列,執行ensurePrestart方法。
void ensurePrestart() {
int wc = workerCountOf(ctl.get());
if (wc < corePoolSize)
addWorker(null, true);
else if (wc == 0)
addWorker(null, false);
}
addWorker已經在ThreadPoolExecutor篇分析過,該方法負責同步將線程池數量+1,并且創建Worker對象添加到HashSet中,最后開啟Worker對象中的線程。因為RunnableScheduledFuture對象已經被添加到任務隊列,Worker中的線程通過getTask方法自然會取到DelayedWorkQueue中的RunnableScheduledFuture任務并執行它的run方法。
這里需要注意的是addWorker方法只在核心線程數未達上限或者沒有線程的情況下執行,并不像ThreadPoolExecutor那樣可以同時存在多個非核心線程,ScheduledThreadPoolExecutor最多只支持一個非核心線程,除非它終止了不會再創建新的非核心線程。
schedule(Callable<V> callable, long delay, TimeUnit unit)
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay,
TimeUnit unit) {
if (callable == null || unit == null)
throw new NullPointerException();
RunnableScheduledFuture<V> t = decorateTask(callable,
new ScheduledFutureTask<V>(callable,
triggerTime(delay, unit)));
delayedExecute(t);
return t;
}
protected <V> RunnableScheduledFuture<V> decorateTask(
Callable<V> callable, RunnableScheduledFuture<V> task) {
return task;
}
ScheduledFutureTask(Callable<V> callable, long ns) {
super(callable);
this.time = ns;
this.period = 0;
this.sequenceNumber = sequencer.getAndIncrement();
}
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
與schedule(Runnable command,long delay,TimeUnit unit)相比除了可以通過ScheduledFutureTask的get方法得到返回值外沒有區別。
scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (period <= 0)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(period));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
與上述兩個方法的區別在于ScheduledFutureTask的構造函數多了參數period,即任務執行的最小周期:
ScheduledFutureTask(Runnable r, V result, long ns, long period) {
super(r, result);
this.time = ns;
this.period = period;
this.sequenceNumber = sequencer.getAndIncrement();
}
scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit)
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (delay <= 0)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(-delay));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
與scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)的區別是參數delay傳入到ScheduledFutureTask的構造方法中是以負數的形式。
小結
四種延時啟動任務的方法除了構造ScheduledFutureTask的參數不同外,運行機制是相同的。先將任務添加到DelayedWorkQueue 中,然后創建Worker對象,啟動內部線程輪詢DelayedWorkQueue 中的任務。
那么DelayedWorkQueue的add方法是如何實現的,線程輪詢DelayedWorkQueue 調用的poll和take方法又如何實現?
回顧getTask方法獲取任務時的代碼片段:
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
如果我們設置ScheduledThreadPoolExecutor的核心線程數量為0,則執行poll方法。而對于核心線程則執行take方法。
下面分析DelayedWorkQueue 的具體實現。
DelayedWorkQueue
static class DelayedWorkQueue extends AbstractQueue<Runnable>
implements BlockingQueue<Runnable> {
private static final int INITIAL_CAPACITY = 16;
private RunnableScheduledFuture<?>[] queue =
new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
private final ReentrantLock lock = new ReentrantLock();
private int size = 0;
}
首先DelayedWorkQueue 是ScheduledThreadPoolExecutor的靜態內部類。它的內部有一個RunnableScheduledFuture數組,且初始容量為16.這里提前說明下,queue 數組儲存的其實是二叉樹結構的索引,這個二叉樹其實就是最小堆。
add方法
public boolean add(Runnable e) {
return offer(e);
}
public boolean offer(Runnable x) {
if (x == null)
throw new NullPointerException();
RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
final ReentrantLock lock = this.lock;
lock.lock();
try {
int i = size;
if (i >= queue.length)
grow();
size = i + 1;
if (i == 0) {
queue[0] = e;
setIndex(e, 0);
} else {
siftUp(i, e);
}
if (queue[0] == e) {
leader = null;
available.signal();
}
} finally {
lock.unlock();
}
return true;
}
private void grow() {
int oldCapacity = queue.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // grow 50%
if (newCapacity < 0) // overflow
newCapacity = Integer.MAX_VALUE;
queue = Arrays.copyOf(queue, newCapacity);
}
private void setIndex(RunnableScheduledFuture<?> f, int idx) {
if (f instanceof ScheduledFutureTask)
((ScheduledFutureTask)f).heapIndex = idx;
}
private void siftUp(int k, RunnableScheduledFuture<?> key) {
while (k > 0) {
int parent = (k - 1) >>> 1;
RunnableScheduledFuture<?> e = queue[parent];
if (key.compareTo(e) >= 0)
break;
queue[k] = e;
setIndex(e, k);
k = parent;
}
queue[k] = key;
setIndex(key, k);
}
在執行add方法時內部執行的是offer方法,添加RunnableScheduledFuture任務到隊列時先通過內部的ReentrantLock加鎖,因此在多線程調用schedule(Runnable command,long delay, TimeUnit unit)添加任務時也能保證同步。
接下來先判斷隊列是否已滿,若已滿就先通過grow方法擴容。擴容算法是將現有容量*1.5,然后將舊的數組復制到新的數組。(左移一位等于除以2)。
然后判斷插入的是否為第一個任務,如果是就將RunnableScheduledFuture向下轉型為ScheduledFutureTask,并將其heapIndex 屬性設置為0.
如果不是第一個任務,則執行siftUp方法。該方法先找到父親RunnableScheduledFuture對象節點,將要插入的RunnableScheduledFuture節點與之compareTo比較,若父親RunnableScheduledFuture對象的啟動時間小于當前要插入的節點的啟動時間,則將節點插入到末尾。反之會對二叉樹以啟動時間升序重新排序RunnableScheduledFuture接口的實現其實是ScheduledFutureTask類:
new ScheduledFutureTask<Void>(command, null,triggerTime(delay, unit);
private long triggerTime(long delay, TimeUnit unit) {
return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
}
/**
* Returns the trigger time of a delayed action.
*/
long triggerTime(long delay) {
return now() +
((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}
final long now() {
return System.nanoTime();
}
第三個參數triggerTime方法返回的就是任務延時的時間加上當前時間。
在ScheduledFutureTask內部實現了compareTo方法:
public int compareTo(Delayed other) {
if (other == this) // compare zero 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 diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
}
比較的兩個任務的啟動時間。所以DelayedWorkQueue內部的二叉樹是以啟動時間早晚排序的。
poll方法
public RunnableScheduledFuture<?> poll(long timeout, TimeUnit unit)
throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
RunnableScheduledFuture<?> first = queue[0];
if (first == null) {
//情況一 空隊列
if (nanos <= 0)
return null;
else
nanos = available.awaitNanos(nanos);
} else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
//情況二 已到啟動時間
return finishPoll(first);
if (nanos <= 0)
//情況三 未到啟動時間,但是線程等待超時
return null;
first = null; // don't retain ref while waiting
if (nanos < delay || leader != null)
nanos = available.awaitNanos(nanos);
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
long timeLeft = available.awaitNanos(delay);
nanos -= delay - timeLeft;
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && queue[0] != null)
available.signal();
lock.unlock();
}
}
非核心線程會通過poll方法同步獲取任務隊列中的RunnableScheduledFuture,如果隊列為空或者在timeout內還等不到任務的啟動時間,都將返回null。如果任務隊列不為空,并且首個任務已到啟動時間線程就能夠獲取RunnableScheduledFuture任務并執行run方法。
take方法
public RunnableScheduledFuture<?> take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
RunnableScheduledFuture<?> first = queue[0];
if (first == null)
available.await();
else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
return finishPoll(first);
first = null; // don't retain ref while waiting
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && queue[0] != null)
available.signal();
lock.unlock();
}
}
與非核心線程執行的poll方法相比,核心線程執行的take方法并不會超時,在獲取到首個將要啟動的任務前,核心線程會一直阻塞。
finishPoll方法
private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
int s = --size;
RunnableScheduledFuture<?> x = queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
setIndex(f, -1);
return f;
}
在成功獲取任務后,DelayedWorkQueue的finishPoll方法會將任務移除隊列,并以啟動時間升序重排二叉樹。
小結
DelayedWorkQueue內部維持了一個以任務啟動時間升序排序的二叉樹數組,啟動時間最靠前的任務即數組的首個位置上的任務。核心線程通過take方法一直阻塞直到獲取首個要啟動的任務。非核心線程通過poll方法會在timeout時間內阻塞嘗試獲取首個要啟動的任務,如果超過timeout未得到任務不會繼續阻塞。
這里要特別說明要啟動的任務指的是RunnableScheduledFuture內部的time減去當前時間小于等于0,未滿足條件的任務不會被take或poll方法返回,這也就保證了未到指定時間任務不會執行。
執行ScheduledFutureTask
前面已經分析了schedule方法如何將RunnableScheduledFuture插入到DelayedWorkQueue,Worker內的線程如何獲取定時任務。下面分析任務的執行過程,即ScheduledFutureTask的run方法:
public void run() {
boolean periodic = isPeriodic();
if (!canRunInCurrentRunState(periodic))
cancel(false);
else if (!periodic)
ScheduledFutureTask.super.run();
else if (ScheduledFutureTask.super.runAndReset()) {
setNextRunTime();
reExecutePeriodic(outerTask);
}
}
如果執行的是非周期型任務,調用ScheduledFutureTask.super.run()方法,即ScheduledFutureTask的父類FutureTask的run方法。FutureTask的run方法已經在ThreadPoolExecutor篇分析過,這里不再多說。
如果執行的是周期型任務,則執行ScheduledFutureTask.super.runAndReset():
protected boolean runAndReset() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return false;
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 {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
return ran && s == NEW;
}
這個方法同run方法比較的區別是call方法執行后不設置結果,因為周期型任務會多次執行,所以為了讓FutureTask支持這個特性除了發生異常不設置結果。
執行完任務后通過setNextRunTime方法計算下一次啟動時間:
private void setNextRunTime() {
long p = period;
if (p > 0)
//情況一
time += p;
else
//情況二
time = triggerTime(-p);
}
long triggerTime(long delay) {
return now() +
((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}
還記得ScheduledThreadPoolExecutor執行定時任務的后兩種scheduleAtFixedRate和scheduleWithFixedDelay。
scheduleAtFixedRate會執行到情況一,下一次任務的啟動時間最早為上一次任務的啟動時間加period。
scheduleWithFixedDelay會執行到情況二,這里很巧妙的將period參數設置為負數到達這段代碼塊,在此又將負的period轉為正數。情況二將下一次任務的啟動時間設置為當前時間加period。
然后將任務再次添加到任務隊列:
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
if (canRunInCurrentRunState(true)) {
super.getQueue().add(task);
if (!canRunInCurrentRunState(true) && remove(task))
task.cancel(false);
else
ensurePrestart();
}
}
ScheduledFuture的get方法
既然ScheduledFuture的實現是ScheduledFutureTask,而ScheduledFutureTask繼承自FutureTask,所以ScheduledFuture的get方法的實現就是FutureTask的get方法的實現,FutureTask的get方法的實現分析在ThreadPoolExecutor篇已經寫過,這里不再敘述。要注意的是ScheduledFuture的get方法對于非周期任務才是有效的。
ScheduledThreadPoolExecutor總結
- ScheduledThreadPoolExecutor是實現自ThreadPoolExecutor的線程池,構造方法中傳入參數n,則最多會有n個核心線程工作,空閑的核心線程不會被自動終止,而是一直阻塞在DelayedWorkQueue的take方法嘗試獲取任務。構造方法傳入的參數為0,ScheduledThreadPoolExecutor將以非核心線程工作,并且最多只會創建一個非核心線程,參考上文中ensurePrestart方法的執行過程。而這個非核心線程以poll方法獲取定時任務之所以不會因為超時就被回收,是因為任務隊列并不為空,只有在任務隊列為空時才會將空閑線程回收,詳見ThreadPoolExecutor篇的runWorker方法,之前我以為空閑的非核心線程超時就會被回收是不正確的,還要具備任務隊列為空這個條件。
- ScheduledThreadPoolExecutor的定時執行任務依賴于DelayedWorkQueue,其內部用可擴容的數組實現以啟動時間升序的二叉樹。
- 工作線程嘗試獲取DelayedWorkQueue的任務只有在任務到達指定時間才會成功,否則非核心線程會超時返回null,核心線程一直阻塞。
- 對于非周期型任務只會執行一次并且可以通過ScheduledFuture的get方法阻塞得到結果,其內部實現依賴于FutureTask的get方法。
- 周期型任務通過get方法無法獲取有效結果,因為FutureTask對于周期型任務執行的是runAndReset方法,并不會設置結果。周期型任務執行完畢后會重新計算下一次啟動時間并且再次添加到DelayedWorkQueue中。
在源代碼的分析過程中發現分析DelayedWorkQueue還需要有二叉樹的升序插入算法的知識,一開始也沒有認出來這種數據結構,后來又看了別人的文章才了解。這里比較難理解,有興趣的同學可以參考《深度解析Java8 – ScheduledThreadPoolExecutor源碼解析》。