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);
}
在任务触发完毕之后,将会重新根据配置的时间间隔加入到阻塞队列后,重复上述的流程。