通过源码分析,我们可以更深入的了解其底层原理。
对于JDK自带的定时器,主要涉及TimerTask类、Timer类、TimerQueue类、TimerThread类,其中TimerQueue和TimerThread类与Timer类位于同一个类文件,由Timer内部调用。
先画上一张图,描述一下Timer的大致模型,Timer的模型很容易理解,即任务加入到任务队列中,由任务处理线程循环从任务队列取出任务执行:
一、TimerTask
TimerTask是一个任务抽象类,实现了Runnable接口,是可被线程执行的。
1. 任务状态
在TimerTask中定义了关于任务状态的常量字段:
// 未调度状态
static final int VIRGIN = 0;
// 任务已调度,但未执行
static final int SCHEDULED = 1;
// 若是一次性任务表示已执行;可重复执行任务,该状态无效
static final int EXECUTED = 2;
// 任务被取消
static final int CANCELLED = 3;
当一个TimerTask对象创建后,其初始状态为VIRGIN;
当调用Timer的schedule方法调度了此TimerTask对象后,其状态变更为SCHEDULED;
如果TimerTask是一次性任务,此任务执行后,状态将变为EXECUTED,可重复执行任务执行后状态不变;
当中途调用了TimerTask.cancel方法,该任务的状态将变为CANCELLED。
2. 任务属性说明
TimerTask中,有如下成员变量:
// 用于加锁控制多线程修改TimerTask内部状态
final Object lock = new Object();
// 任务状态,初始状态为待未调度状态
int state = VIRGIN;
// 任务的下一次执行时间点
long nextExecutionTime;
// 任务执行的时间间隔。正数表示固定速率;负数表示固定时延;0表示只执行一次
long period = 0;
3. 任务方法说明
TimerTask中有三个方法:
- run:实现了Runnable接口,创建TimerTask需要重写此方法,编写任务执行代码
- cancel:取消任务
- scheduledExecutionTime:计算执行时间点
3.1. Cancel方法
cancel方法的实现代码:
public boolean cancel() {
synchronized(lock) {
boolean result = (state == SCHEDULED);
state = CANCELLED;
return result;
}
}
在cancel方法内,使用synchronized加锁,这是因为Timer内部的线程会对TimerTask状态进行修改,而调用cancel方法一般会是另外一个线程。
为了避免线程同步问题,cancel在修改状态前进行了加锁操作。
调用cancel方法将会把任务状态变更为CANCELLED状态,即任务取消状态,并返回一个布尔值,该布尔值表示此任务之前是否已是SCHEDULED 已调度状态。
3.2. scheduledExecutionTime方法
scheduledExecutionTime方法实现:
public long scheduledExecutionTime() {
synchronized(lock) {
return (period < 0 ? nextExecutionTime + period
: nextExecutionTime - period);
}
}
该方法返回此任务的下次执行时间点。
二、Timer
分析Timer源代码,Timer在内部持有了两个成员变量:
private final TaskQueue queue = new TaskQueue();
private final TimerThread thread = new TimerThread(queue);
TaskQueue是任务队列,TimerThread是任务处理线程。
1. sched方法
无论是使用schedule还是scheduleAtFixedRate方法来调度任务,Timer内部最后都是调用sched方法进行处理。
public void schedule(TimerTask task, Date time) {
sched(task, time.getTime(), 0); // 一次性任务,period为0
}
public void schedule(TimerTask task, long delay) {
...
sched(task, System.currentTimeMillis()+delay, 0); // 一次性任务,period为0
}
public void schedule(TimerTask task, long delay, long period) {
...
sched(task, System.currentTimeMillis()+delay, -period); // 固定延时模式,-period
}
public void schedule(TimerTask task, Date firstTime, long period) {
...
sched(task, firstTime.getTime(), -period); // 固定延时模式,-period
}
public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
...
sched(task, System.currentTimeMillis()+delay, period); // 固定速率模式,period为正
}
public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) {
...
sched(task, firstTime.getTime(), period); // 固定速率模式,period为正
}
sched方法核心代码:
private void sched(TimerTask task, long time, long period) {
...
// 加锁,避免外部其他线程同时调用cancel,同时访问queue产生线程同步问题
synchronized(queue) {
// 如果线程已终止,抛出异常
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");
// 加锁,避免多线程访问同一个任务产生线程同步问题
synchronized(task.lock) {
// task的状态必须为VIRGIN,否则认为已经加入调度或者已经取消了,避免重复的调度
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
// 设置下次执行时间点
task.nextExecutionTime = time;
// 设置时间间隔
task.period = period;
// 任务状态变更为已调度
task.state = TimerTask.SCHEDULED;
}
// 将任务添加到队列中
queue.add(task);
// 如果此任务是最近的任务,唤醒线程
if (queue.getMin() == task)
queue.notify();
}
}
2. cancel方法
cancel方法一般是由外部其他线程调用,而Timer内部的线程也会对任务队列进行操作,因此加锁。
public void cancel() {
synchronized(queue) {
// 修改线程的循环执行标志,令线程能够终止
thread.newTasksMayBeScheduled = false;
// 清空任务队列
queue.clear();
// 唤醒线程
queue.notify();
}
}
3. purge方法
当通过TimerTask.cancel将任务取消后,Timer的任务队列还引用着此任务,Timer只有到了要执行时才会移除,其他时候并不会自动将此任务移除,需要调用purge方法进行清理。
public int purge() {
int result = 0;
synchronized(queue) {
// 遍历队列,将CANCELLED状态的任务从任务队列中移除
for (int i = queue.size(); i > 0; i--) {
if (queue.get(i).state == TimerTask.CANCELLED) {
queue.quickRemove(i);
result++;
}
}
// 如果移除任务数不为0,触发重新排序
if (result != 0)
queue.heapify();
}
// 返回移除任务数
return result;
}
三、TaskQueue
TaskQueue是Timer类文件中封装的一个队列数据结构,内部默认是一个长度128的TimerTask数组,当任务加入时,检测到数组将满将会自动扩容1倍,并对数组元素根据下次执行时间nextExecutionTime按时间从近到远进行排序。
void add(TimerTask task) {
// 检测数组长度,若不够则进行扩容
if (size + 1 == queue.length)
queue = Arrays.copyOf(queue, 2*queue.length);
// 任务入队
queue[++size] = task;
// 排序
fixUp(size);
}
fixUp方法实现:
private void fixUp(int k) {
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;
}
}
TaskQueue中除了fixUp方法外还有一个fixDown方法,这两个其实就是堆排序算法,在算法专题中再进行详细介绍,只要记住他们的任务就是按时间从近到远进行排序,最近的任务排在队首即可。
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;
}
}
void heapify() {
for (int i = size/2; i >= 1; i--)
fixDown(i);
}
四、TimerThread
TimerThread的核心代码位于mainLoop方法:
private void mainLoop() {
// 死循环,从队列取任务执行
while (true) {
try {
TimerTask task;
boolean taskFired;
// 对任务队列加锁
synchronized(queue) {
// 如果队列中没有任务,则进入等待,newTasksMayBeScheduled是线程运行标志位,为false时将退出循环
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
// 如果任务队列是空的还执行到这一步,说明newTasksMayBeScheduled为false,退出循环
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)) {
// 如果任务period=0,即一次性任务
if (task.period == 0) {
// 从队列移除一次性任务
queue.removeMin();
// 任务状态变更为已执行
task.state = TimerTask.EXECUTED;
} else {
// 可重复执行任务,重新进行调度,period<0是固定时延,period>0是固定速率
queue.rescheduleMin(
task.period<0 ? currentTime - task.period // 计算下次执行时间
: executionTime + task.period);
}
}
}
// taskFired为false即任务尚未到执行时间点,进行等待,等待时间是 执行时间点 - 当前时间点
if (!taskFired)
queue.wait(executionTime - currentTime);
}
// taskFired为true表示已触发,执行任务
if (taskFired)
task.run();
} catch(InterruptedException e) {
}
}
}