最近在做一个给商户定时更新标签的项目,用的是公司的crane框架,非常好奇其底层是如何实现任务的定时调度的。追到底层,无非是java原生的定时任务类Timer或ScheduledExecutorService(crane框架底层是ScheduledExecutorService实现的)。于是,抽空研究了一下Timer和ScheduledExecutorService类的源码,发现还是有很多蛮好玩的地方的。这篇文章仅介绍Timer类的实现。
一个Timer实例使用唯一的一个线程TimerThread去依次执行任务队列TaskQueue中的所有定时任务,每次调用Timer中的schedule方法,就会将不同的定时任务TimerTask加入到这个TaskQueue中。该任务队列使用数组存储定时任务,但是不要小看这个数组哦,这里维护着一个根据距离下一次执行的时间长短排序的平衡二叉树结构。因此,马上要执行的定时任务就是数组的第一个位置。采用这种数据结构的好处是:可以在常数时间复杂度内获取到最快要执行的定时任务,且插入新任务、移除马上要执行的任务和调整马上要执行的任务的执行时间等操作的时间复杂度都是O(logn)。
可以看到,Timer并不能保证准时地执行定时任务,因为唯一的一个执行线程需要依次执行任务队列中的任务,只有保证每个执行任务的执行时间很短,短到不会影响其他任务执行,才能做到真正的准时调度。在一点上,ScheduLedScheduledExecutor无疑会做得更好一些,它维护了一个线程池去执行定时任务,但是也无法保证准时执行。
Timer可以在构造实例时设置isDaemon,以决定执行线程采用守护线程还是普通线程。
接下来,我们来分析一下Timer最核心的一段源码,即TimerThread中的run()方法调用的方法:
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
//当任务队列为空且Timer没有被取消,一直等待直到有定时任务。newTasksMayBeScheduled会在cancel方法被调用时置为false。
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
//如果Timer被取消或定时任务异常时,才会走到这里。此时跳出循环。不再进行定时任务调度。
if (queue.isEmpty())
break;
// 找到下一个需要执行的定时任务,重新设置下一次被调度的时间,触发任务队列平衡二叉树结构顺序调整。
long currentTime, executionTime;
task = queue.getMin();
synchronized(task.lock) {
//如果该定时任务被取消了,则从任务队列中移除
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue;
}
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if (taskFired = (executionTime<=currentTime)) {
if (task.period == 0) {
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else {
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
//如果下一个需要执行的任务没有到执行时间,则等待
if (!taskFired)
queue.wait(executionTime - currentTime);
}
//如果下一个需要执行的任务的执行时间到了,则执行TimerTask的run方法。注意:此时并没有持有queue对应的对象锁。
if (taskFired)
task.run();
} catch(InterruptedException e) {
}
}
}