ScheduledThreadPoolExecutor的schedule()方法可以將線程任務在一段時間後定時觸發,並在一段時間後反覆定時觸發。
在ScheduledThreadPoolExecutor,定時任務都會被包裝成一個ScheduledFutureTask,以下是其構造方法。
ScheduledFutureTask(Runnable r, V result, long ns, long period) {
super(r, result);
this.time = ns;
this.period = period;
this.sequenceNumber = sequencer.getAndIncrement();
}
其time則是其第一次觸發距離當前的時間,period則是後續觸發之間的間隔,sequenceNumber則作爲阻塞隊列中根據觸發倒數時間排序的時候,當時間一致的時候,通過這個數來確定其入隊列的先後順序,來判定觸發順序,這是一個自增量。
當通過schedule()方法準備將定時任務加入到線程池中的時候,其實發生的邏輯非常簡單。
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;
}
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();
}
}
void ensurePrestart() {
int wc = workerCountOf(ctl.get());
if (wc < corePoolSize)
addWorker(null, true);
else if (wc == 0)
addWorker(null, false);
}
真正發生的邏輯主要還是先將投入的線程任務已經第一次觸發時間和觸發間隔包裝成了上文提到的ScheduledFutureTask,而具體的下一次觸發時間也被根據時間單位轉換成了毫秒。
而後將該任務交給阻塞隊列,之後根據當前線程池的實際情況添加工作線程,schedule()方法的主要邏輯就宣告結束。
由此可以看到,定時任務的實現主要由該線程池的阻塞隊列來實現。
在ScheduledThreadPoolExecutor中,指定的阻塞隊列爲DelayedQueue。
DelayedQueue表面是一個阻塞隊列,其容器也爲數組實現,但其最終實現爲一個小根堆。
在通過offer()方法入堆的時候,如果該堆中已經存在數據,那麼就會從該任務作爲該堆的最後一個節點開始,不斷通過siftUp()方法,通過堆排序,直到將該任務的觸發時間作爲比較標準放到小根堆中一個合適的位置上。
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);
}
在這裏,會不斷取得當前節點的父節點,並不斷比較,直到其父節點的觸發時間小於當前節點,否則交換位置,繼續與父節點比較,直到到根節點爲止。
如果新加入的任務被放到了根節點上,那麼這個任務將是最近的被觸發的任務,如果相比加入前,發生了改變將會通知所有工作線程重新嘗試競爭根節點的任務。
由此可見,當新的線程定時任務被加入到線程池,實則是加入到了線程池的阻塞隊列當中,而阻塞隊列則是一個小根堆,將會把最近會觸發的定時任務放入到根節點。
之後,則是工作線程的邏輯。
在線程池中,工作線程爲通過阻塞隊列的task()方法進行拉取任務。
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();
}
}
工作線程拉取任務,主要分爲以下四種情況。
1.當前阻塞隊列沒有任務,等待新的任務加入而阻塞。
2.成功獲取到了根節點的任務,獲得當前任務的觸發時間並阻塞對應時間到觸發,此時該線程回座位leader。
3.當前leader線程已存在,阻塞,直到leader線程結束等待並取走根節點的任務觸發,則繼續嘗試獲取根節點的任務。
4.在2的情況下,根節點的任務發生了改變取消阻塞,重新開始嘗試獲取根節點的任務。
當根節點的任務被取走之後,將會把根節點的任務取走,此時就需要重新進行堆排序,由於此時只需要選擇出最小,從上往下進行堆排序更加適合。
private void siftDown(int k, RunnableScheduledFuture<?> key) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
RunnableScheduledFuture<?> c = queue[child];
int right = child + 1;
if (right < size && c.compareTo(queue[right]) > 0)
c = queue[child = right];
if (key.compareTo(c) <= 0)
break;
queue[k] = c;
setIndex(c, k);
k = child;
}
queue[k] = key;
setIndex(key, k);
}
在任務觸發完畢之後,將會重新根據配置的時間間隔加入到阻塞隊列後,重複上述的流程。