在程序中简单实用Timer的方法,参考学习。
定时任务,也叫定时器,是指在指定的时间执行指定任务的机制,类似于Windows自带的计划任务。JDK中提供了定时器的支持—java.util.Timer,下面我们来系统学习一下它的实现原理。
Timer主要由三个部分组成: 任务TimerTask、任务队列TaskQueue和 任务调试者TimerThread。多个任务单元 TimerTask按照一定的优先级组成了任务队列TaskQueue,任务调度TimerThread按照一定的规则每次取出任务队列中的一个任务进行处理。
TimerTask 任务单元
//*********任务状态*****************
//VIRGIN表示Task刚刚被创建
static final int VIRGIN = 0;
//************几个状态常量******************
//SCHEDULED表示Task已经被加入TaskQueue中,等待调度
static final int SCHEDULED = 1;
//EXECUTED表示Task已经被执行
static final int EXECUTED = 2;
//CANCELLED表示Task已经被取消
//**************两个重要的成员变量******************
//nextExecutionTime这个成员变量用到记录该任务下次执行时间, 其格式和System.currentTimeMillis()一致.这个值是作为任务队列中任务排序的依据. 任务调试者执行每个任务前会对这个值作处理,重新计算下一次任务执行时间,并为这个变量赋值.
long nextExecutionTime;
//period 用来描述任务的执行方式: 0表示不重复执行的任务. 正数表示固定速率执行的任务. 负数表示固定延迟执行的任务. (固定速率: 不考虑该任务上一次执行情况,始终从开始时间算起的每period执行下一次。固定延迟: 考虑该任务一次执行情况,在上一次执行后period执行下一次)。
long period = 0;
TaskQueue任务队列
TaskQueue是用来保存TimerTask的队列,是一个数组, 采用平衡二叉堆来实现优先级调度, 并且是一个最小堆, 这个堆中queue[n] 的孩子是queue[2*n] 和 queue[2*n+1]。任务队列的优先级按照TimerTask类的成员变量nextExecutionTime值来排序。在任务队列中, nextExecutionTime最小就是所有任务中最早要被调度来执行的(也就是堆顶的Task), 所以被安排在queue[1] (假设任务队列非空),对于堆中任意一个节点m,和他的任意子孙节点n,一定遵循: m.nextExecutionTime <= n.nextExecutionTime.
下面是TaskQueue的核心代码,也是最小堆的实现代码:
添加任务:
/**
*首先会判断是否已经满了,如果已经满了, 那么容量扩大至原来2倍, 然后将需要添加的任务放到队列最后. 之后就会调用fixUp 方法来进行队列中任务优先级调整.
*/
void add(TimerTask task) {
if (size + 1 == queue.length)
queue = Arrays.copyOf(queue, 2 * queue.length);
queue[++size] = task;
fixUp(size);
}
/**
* fixUp方法的作用是尽量将队列中指定位置(k)的任务向队列前面移动, 即提高它的优先级. 因为新加入的方法很有可能比已经在任务队列中的其它任务要更早执行.
*/
private void fixUp(int k) {
while (k > 1) {
int j = k >> 1; // 对于正数,右移位 <==> j = k/2, 所以j的位置就是k的父亲节点
if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
break;
TimerTask tmp = queue[j];
queue[j] = queue[k];
queue[k] = tmp;
k = j;
}
}
总结:这个过程可以这个描述: 不断地将k位置上元素和它的父亲进行比较, 如果发现孩子节点比父亲小的时候, 那么将父亲和孩子位置互换. 直到最小的来到队列第一个位置。
移除任务:
/**
* 首先直接将当前任务队列中最后一个任务赋给queue[1], 然后将队列中任务数量--, 最后和上面类似, 但是这里是调用fixDown(int k)方法了, 尽量将k位置的任务向队列后面移动.
*/
void removeMin() {
queue[1] = queue[size];
queue[size--] = null; // Drop extra reference to prevent memory leak
fixDown(1);
}
/**
* 将k位置的元素向堆底方向移动
* 1. j = k << 1, .<br>
* 2. 将 j 精确定位到较小的儿子.<br>
* 3. 然后k与j比较,如果k大于j的话, 那么互换<
* 4.继续...
*/
private void fixDown(int k) {
int j;
// 如果还没有到队列的最后,并且没有溢出( j > 0 ),在没有出现溢出的情况下, j = k << 1 等价于 j = 2 * k,将j定位到儿子中
while ((j = k << 1) <= size && j > 0) {
// 找到k的两个孩子中小的那个.
if (j < size && queue[j].nextExecutionTime > queue[j + 1].nextExecutionTime)
j++;
// 找到这个较小的孩子后,(此时k是父亲,j是较小的儿子),父亲和儿子互换位置,即k和j换位子.这样一直下去就可以将这个较大的queue[1]向下堆底移动了.
if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)
break;
TimerTask tmp = queue[j];
queue[j] = queue[k];
queue[k] = tmp;
k = j;
}
}
TimerThread任务调度
TimerThread就是用来调度TaskQueue中的任务的线程。关于任务调度主要有一个成员变量 newTasksMayBeScheduled和调度方法mainLoop()。
boolean newTasksMayBeScheduled = true;
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired = false;
synchronized (queue) {
while (queue.isEmpty() && newTasksMayBeScheduled) {
queue.wait();
}
if (queue.isEmpty())
break; // 直接挑出mainLoop了.
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) { // period表示不重复,移除出队列
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { //重复执行的设置重新调度
queue.rescheduleMin(task.period < 0 ? currentTime - task.period : executionTime
+ task.period);
}
}
}//释放锁
if (!taskFired)
queue.wait(executionTime - currentTime);
}
if (taskFired) // Task fired; run it, holding no locks
task.run();
} catch (InterruptedException e) {
}
}// while(true)
}
newTasksMayBeScheduled变量用来表示是否需要继续等待新任务了。默认情况这个变量是true , 并且这个变量一直是true的。只有两种情况的时候会变成 false :
1.当调用Timer的cancel方法
2.没有引用指向Timer对象了.
任务调度mainLoop()方法中的一个while可以理解为一次任务调度:
STEP 1 : 判断任务队列中是否还有任务, 如果任务队列为空了, 但是newTasksMayBeScheduled变量还是true, 表明 需要继续等待新任务, 所以一直等待。
STEP 2 : 等待唤醒后, 再次判断队列中是否有任务. 如果还是没有任务,那么直接结束定时器工作了.因为queue只在两个地方被调用: addTask和cancel 1.向任务队列中增加任务会唤醒 2.timer.cancel()的时候也会唤醒. 那么这里如果还是empty,那么就是cancel的唤醒了,所以可以结束timer工作了。
STEP 3 : 从任务队列中取出第一个任务,即nextExecutionTime最小的那个任务。
STEP 4: 判断这个任务是否已经被取消. 如果已经被取消了,那么就直接从任务队列中移除这个任务(removeMin() ),然后直接进入下一个任务调度周期。
STEP 5 : 判断是否到了或者已经超过了这个任务应该执行的时间了。如果到了 , 不会立即执行它,而是会在这次循环的最后来执行它。
这里做的事情可以看作是为下一个调度周期进行准备:包括:
1. 判断是否是重复(repeating)任务,如果 task.period == 0, 那么就不是重复任务,所以可以直接将这个任务从任务队列中移除了(removeMin() ),因为没有必要留到下一个调度周期中去了.
2. 如果是需要重复执行的任务, 那么就要重新设置这个任务的nextExecutionTime,即调用方法queue.rescheduleMin(long) ,这个方法中会调用fixDown(1) 负责重新调整任务队列的优先级顺序.
如果还没有到执行时间 , 一直等到 queue.wait(executionTime - currentTime),并且等待完毕后,似乎可以开始运行了, 但是这里设计成不立即运行,而是直接进入下一个任务调度周期.(因为taskFired =false,所以不会在这次进行执行的.)
STEP6: 开始调用任务的run方法运行任务。
还有一点需要注意:
在step2中我们学习到,TimerThread的调度核心是起一个while循环,不断检查是否有task需要执行,其中两次调用了queue.wait()方法。1.向任务队列中增加任务会唤醒 2.timer.cancel()的时候也会唤醒,两种情况下queue.notify()方法会被调用。
但是是否上面两种情况调用notify就已经足够了?当queue为空,并且没人调用add或cancel方法时,TimerThread永远都不会stop。不用担心,对于这个地方的处理JDK加上了一种比较保险的方法:
/**
* This object causes the timer's task execution thread to exit
* gracefully when there are no live references to the Timer object and no
* tasks in the timer queue. It is used in preference to a finalizer on
* Timer as such a finalizer would be susceptible to a subclass's
* finalizer forgetting to call it.
*/
private final Object threadReaper = new Object() {
protected void finalize() throws Throwable {
synchronized(queue) {
thread.newTasksMayBeScheduled = false;
queue.notify(); // In case queue is empty.
}
}
};
用到了Object对象的finalize方法,大家都知道finalize方法是对象被GC的时候调用的。上述做法的思路是:当一个Timer已经没有任何对象引用时,自然不会有新的Task加入到队列中,Timer对象自然也就会被垃圾回收,此时TimerThread也就应该stop了,所以在垃圾回收的时候还应该把newTasksMayBeScheduled设置为false,并且唤起正在wait的TimerThread线程。所以说,如果你创建的Timer不再需要了,最好是调用cancel接口手动取消,否则的话TimerThread就需要等到垃圾回收的时候才会stop。