记
最近看了一下java定时调度Timer的源码,源码简单、清晰,结构清除,对于了解任务调度入门有帮助,因此在此对源码阅读总结一下。
综述
java 的Timer是一个定时任务调度控制器, 在java 并行库中的ScheduledExecutorService面世前,java中主要通过Timer应用实现定时时任务调度, 在Timer的定时任务调度框架中主要由Timer, Taskqueue, TimerThread, TimerTask组成,Timer是调度器对外的接口,提供对外的任务添加以及调度中心管理接口,TaskQueue是任务下次执行时间的优先级队列,该优先级队列是小顶堆结构实现的。TimerThread是任务执行的主体 ,每次从任务队列中取出下一个需要执行的任务,在该任务下次执行时间到来时开始执行,TimerTask是一个定时调度任务的抽象类,通过state对任务状态管理。
类关系图
如下图所示,为Timer中各个模块的类图
总体时序图
模块详解
Timer
Timer类在构造函数里面会启动任务执行线程,在之后,用户就可以通过调用schedule接口将任务添加进任务队列,等待线程调用,因此Timer的主要任务就是添加定时任务,Timer提供了几个schedule函数,各个schedule函数在参数有不同,主要是做参数验证,而各个schedule对任务的添加最终归拢到了一个函数shed。
@param time 开始执行时间
@param period 任务间隔
private void sched(TimerTask task, long time, long period) {
synchronized(queue) {
synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException
//任务调度初始化
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}
//添加task到队列中
queue.add(task);
//如果通知队列有任务,与TimerThread的queue wait相对应
if (queue.getMin() == task)
queue.notify();
}
}
定时任务中,通过period控制任务时间点,period的不同值有不同含义,0表示一次性任务,大于0表示时间以上次执行开始时间为基准,小于0表示以上次执行完毕时间为基准。举个例子,一个任务执行时间为1s,如果period=5,则该任务的执行时间点为:0,5,10, 15;而如果period=-5,则执行时间点为:0,6,12,18。
由上面的代码知道,Timer是线程安全的,对资源queue,以及task的操作都加了锁。
TimerThread
TimerThread是任务的调度执行主体,线程循环从队列中获取最近需要执行的任务,等待该任务执行时间点执行任务
private void mainLoop() {
while (true) {
synchronized(queue) {
// Wait for queue to become non-empty
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
task = queue.getMin();
synchronized(task.lock) {
//任务已经被取消,从队列中删除
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}
//当前任务是否可以运行
if (taskFired = (executionTime<=currentTime)) {
//一次性任务,移除
if (task.period == 0) {
remove task
} else {
//安排下次执行时间点
reschedule task
}
}
}
//还未到达任务执行时间点,需要等待,或者被唤醒
if (!taskFired)
queue.wait(executionTime - currentTime);
}
//执行任务
if (taskFired)
task.run();
} catch(InterruptedException e) {
//中断退出调度
}
}
}
TaskQueue
TaskQueue是用栈实现的优先级队列,队列中获取下一个执行的任务,是从栈中获取开始执行时间最早的任务,执行完成以后,该任务要么被移除,要么被重新安排调度计划加入队列中,无论是删除还是重新安排加入都会调整栈。该栈的实现是以一个从1开始索引的数组实现的,满足n元素的子节点是2n,2n+1,根据栈的规则,满足p[n]>p[2n] 以及p[n]>p[2n+1]。每次添加元素,在队尾添加,然后向上迭代调整,每次从栈顶取元素,删除栈顶元素是将队尾元素放到栈顶,而后迭代向下调整。对于该栈的维护,核心是向下调整以及向上调整,向下调整将栈顶元素放置还是位置;向上调整,将队尾元素调整到合适位置。下面是向上调整以及向下调整代码:
向下调整
private void fixDown(int k) {
int j;
//直至当前节点小于两个子节点,或者已经是叶子节点
while ((j = k << 1) <= size && j > 0) {
if (j < size &&
queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)
j++; // j indexes smallest kid
if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)
break;
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
k = j;
}
}
向上调整
while (k > 1) {
int j = k >> 1; //父节点
//直至父节点小于当前节点
if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
break;
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
k = j;
}
总结
Timer的虽然代码简单,但是里面却涵盖了信号量、锁、线程、栈、优先级队列以及调度等知识,而且代码中在栈的实现里面有些巧妙的处理,如通过移位取代除2的操作,因此代码有很大阅读价值