本文是我在项目中使用Timer后, 又看了一下Timer实现原理的整理. 主要介绍JKD自带的 java.util.Timer 定时器的实现原理. Timer使用本身很简单, 同样, 他的设计原理也很精妙.如果你仅仅只是想知道如何在自己的程序中来使用java.util.Timer的一些方法,那么请移步: http://robinsoncrusoe.iteye.com/blog/986320
API介绍在这里: http://online.chinaitpower.com/api/jdk150/java/util/Timer.html
Timer和TimerTask Since JDK1.3
Timer中最主要由三个部分组成: 任务 TimerTask 、 任务队列: TaskQueue queue 和 任务调试者:TimerThread thread, 他们之间的关系可以通过下面图示:
在这个图中,可以清楚地看到这Timer本身及其和这三个部分的关系:
1. Timer可以看作是面向开发人员的一个"接口"
2. 所有向Timer添加的任务都会被放入一个TaskQueue类型的任务队列中去.(如何安排任务优先级顺序下文会讲)
3. 任务调度由TimerThread负责.
任务单元 TimerTask
首先看一下任务单元实体类: TimerTask. 在这个类中, 要关注的是任务状态和几个状态常量:
- /** 标识任务的状态 */
- int state = VIRGIN;
- /** 任务的状态的常量 */
- static final int VIRGIN = 0;
- static final int SCHEDULED = 1;
- static final int EXECUTED = 2;
- static final int CANCELLED = 3;
以及一个比较重要的两个成员变量:
- long nextExecutionTime;
- long period = 0;
nextExecutionTime 这个成员变量用到记录该任务下次执行时间, 其格式和System.currentTimeMillis()一致.这个值是作为任务队列中任务排序的依据. 任务调试者执行每个任务前会对这个值作处理,重新计算下一次任务执行时间,并为这个变量赋值.
period 用来描述任务的执行方式: 0表示不重复执行的任务. 正数表示固定速率执行的任务. 负数表示固定延迟执行的任务. (固定速率: 不考虑该任务上一次执行情况,始终从开始时间算起的每period执行下一次. 固定延迟: 考虑该任务一次执行情况,在上一次执行后period执行下一次).
任务队列 TaskQueue
事实上任务队列是一个数组, 采用平衡二叉堆来实现他的优先级调度, 并且是一个小顶堆. 需要注意的是, 这个堆中queue[n] 的孩子是queue[2*n] 和 queue[2*n+1].
任务队列的优先级按照TimerTask类的成员变量nextExecutionTime值来排序(注意, 这里的任务指的是那些交由定时器来执行的, 继承TimerTask的对象).
在任务队列中, nextExecutionTime最小就是所有任务中最早要被调度来执行的, 所以被安排在queue[1] (假设任务队列非空).
对于堆中任意一个节点n, 和他的任意子孙节点d,一定遵循: n.nextExecutionTime <= d.nextExecutionTime.
1. 添加任务
- void add(TimerTask task) {
- if (size + 1 == queue.length)
- queue = Arrays.copyOf(queue, 2 * queue.length);
- queue[++size] = task;
- fixUp(size);
- }
首先会判断是否已经满了,(任务队列的初始容量是128 ),如果已经满了, 那么容量扩大至原来2倍, 然后将需要添加的任务放到队列最后. 之后就会调用fixUp 方法来进行队列中任务优先级调整. 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位置上元素和它的父亲进行比较, 上文也提到过了. 由于必须满足 "对于堆中任意一个节点n, 和他的任意子孙节点d,一定遵循: n.nextExecutionTime <= d.nextExecutionTime.", 那么在不断比较过程中, 如果发现孩子节点比父亲小的时候, 那么将父亲和孩子位置互换. 直到来到队列第一个位置.
2. 移除任务
- void removeMin() {
- queue[1] = queue[size];
- queue[size--] = null; // Drop extra reference to prevent memory leak
- fixDown(1);
- }
从任务队列中移除一个任务的过程, 首先直接将当前任务队列中最后一个任务赋给queue[1], 然后将队列中任务数量--, 最后和上面类似, 但是这里是调用fixDown(int k)方法了, 尽量将k位置的任务向队列后面移动.
- /**
- * -将k位置的元素向堆底方向移动.<br>
- * 1. j = k << 1, 将j定位到儿子中.<br>
- * 2. 将 j 精确定位到较小的儿子.<br>
- * 3. 然后k与j比较,如果k大于j的话, 那么互换<br>
- * 4.继续...
- */
- private void fixDown(int k) {
- int j;
- // 如果还没有到队列的最后,并且没有溢出( j > 0 )
- // 在没有出现溢出的情况下, j = k << 1 等价于 j = 2 * k ;
- while ((j = k << 1) <= size && j > 0) {
- // 找到k的两个孩子中小的那个.
- if (j < size && queue[j].nextExecutionTime > queue[j + 1].nextExecutionTime)
- j++; // j indexes smallest kid
- // 找到这个较小的孩子后,(此时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
关于任务调度主要要讲下一个成员变量 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) { // Non-repeating, remove
- queue.removeMin();
- task.state = TimerTask.EXECUTED;
- } else { // Repeating task, reschedule
- 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,所以不会在这次进行执行的.)
STEP: 6 开始调用任务的run方法运行任务.