使用 Java 来调度定时任务时,我们经常会使用 Timer 类搞定。Timer 简单易用,其源码阅读起来也非常清晰,本节我们来仔细分析一下 Timer 类,来看看 JDK 源码的编写者是如何实现一个稳定可靠的简单调度器。
Timer 使用
Timer 调度任务有一次性调度和循环调度,循环调度有分为固定速率调度(fixRate)和固定时延调度(fixDelay)。固定速率就好比你今天加班到很晚,但是到了第二天还必须准点到公司上班,如果你一不小心加班到了第二天早上 9 点,你就连休息的时间都没有了。而固定时延的意思是你必须睡够 8 个小时再过来上班,如果你加班到凌晨 6 点,那就可以下午过来上班了。固定速率强调准点,固定时延强调间隔。
Timer timer = new Timer();
TimerTask task = new TimerTask() {
public void run() {
System.out.println("wtf");
}
};
// 延迟 1s 打印 wtf 一次
timer.schedule(task, 1000)
// 延迟 1s 固定时延每隔 1s 周期打印一次 wtf
timer.schedule(task, 1000, 1000);
// 延迟 1s 固定速率每隔 1s 周期打印一次 wtf
timer.scheduleAtFixRate(task, 1000, 1000)
内部结构
Timer 类里包含一个任务队列和一个异步轮训线程。任务队列里容纳了所有待执行的任务,所有的任务将会在这一个异步线程里执行,切记任务的执行代码不可以抛出异常,否则会导致 Timer 线程挂掉,所有的任务都没得执行了。单个任务也不易执行时间太长,否则会影响任务调度在时间上的精准性。比如你一个任务跑了太久,其它等着调度的任务就一直处于饥饿状态得不到调度。所有任务的执行都是这单一的 TimerThread 线程。
class Timer {
TaskQueue queue = new TaskQueue();
TimerThread thread = new TimerThread(queue);
}
堆排序
Timer 的任务队列 TaskQueue 是一个特殊的队列,它内部是一个数组。这个数组会按照待执行时间进行堆排序,堆顶元素总是待执行时间最小的任务。轮训线程会每次轮训出时间点最近的并且到点的任务来执行。数组会自动扩容,如果任务非常多。
class TaskQueue {
TimerTask[] queue = new TimerTask[128];
int size;
}
synchronized(queue) {
...
}
任务状态
TimerTask 有 4 个状态,VIRGIN 是默认状态,刚刚实例化还没有被调度。SCHEDULED 表示已经将任务塞进 TaskQueue 等待被执行。EXECUTED 表示任务已经执行完成。CANCELLED 表示任务被取消了,还没来得及执行就被人为取消了。
abstract class 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; // 间隔
}
对于一个循环任务来说,它不存在 EXECUTED 状态,因为它每次刚刚执行完成,就被重新调度了。EXECUTED 状态仅仅存在于一次性任务,而且这个状态其实并不是表示任务已经执行完成,它是指已经从任务队列里摘出来了,马上就要执行。
任务间隔字段 period 比较特殊,当使用固定速率时,period 为正值,当使用固定间隔时,period 为负值,当任务是一次性时,period 为零。下面是循环任务的下次调度时间设定
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
// 固定时延基于 currentTime 顺延
// 固定速率基于 executionTime(设定时间) 顺延
// next_exec_time = exec_time + period = first_delay + n * period
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
任务锁
Timer 的任务支持取消操作,取消任务的线程和执行任务的线程极有可能不是一个线程。有可能任务正在执行中,结果另一个线程表示要取消任务。这时候 Timer 是如何处理的呢?在 TimerTask 类里看到了一把锁。当任务属性需要修改的时候,都会加锁。
abstract class TimerTask {
final Object lock = new Object();
}
// 取消任务
public boolean cancel() {
synchronized(lock) {
boolean result = (state == SCHEDULED);
state = CANCELLED;
return result;
}
}
// 调度任务
private void sched(TimerTask task, long time, long period) {
synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}
}
// 运行任务
private void mainLoop() {
while(true) {
synchronized(task.lock) {
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue;
}
...
if(task.period == 0) {
task.state = TimerTask.EXECUTED;
}
...
}
task.run();
}
}
public int purge() {
int result = 0;
// 灭掉 CANCELLED 状态的任务
synchronized(queue) {
for (int i = queue.size(); i > 0; i--) {
if (queue.get(i).state == TimerTask.CANCELLED) {
queue.quickRemove(i);
result++;
}
}
}
// 堆调整
if (result != 0)
queue.heapify();
}
return result;
}
任务队列空了
任务队列里没有任务了,调度线程必须按一定的策略进行睡眠。它需要睡眠一直到最先执行的任务到点时立即醒来,所以睡眠截止时间就是第一个任务将要执行的时间。同时在睡觉的时候,有可能会有新的任务被添加进来,它的调度时间可能会更加提前,所以当有新的任务到来时需要可以唤醒正在睡眠的线程。
private void mainLoop() {
while(true) {
...
task = queue.getMin();
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if(executionTime > currentTime) {
// 开始睡大觉
queue.wait(executionTime - currentTime);
}
...
}
}
// 新任务进来了
private void sched(TimerTask task, long time, long period) {
...
queue.add(task);
if (queue.getMin() == task)
queue.notify(); // 唤醒轮训线程
}
Timer 终止
Timer 提供了 cancel() 方法清空队列,停止调度器,不允许有任何新任务进来。它会将 newTasksMayBeScheduled 字段设置为 false 表示 Timer 即将终止。
class TimerThread {
...
boolean newTasksMayBeScheduled; // 终止的标志
...
}
public void cancel() {
synchronized(queue) {
thread.newTasksMayBeScheduled = false;
queue.clear();
queue.notify();
}
}
private void sched(TimerTask task, long time, long period) {
synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");
...
}
}
private void mainLoop() {
while(true) {
// 正常清空下,队列空了,轮训线程会休眠
// 但是如果 newTasksMayBeScheduled 为 false
// 那么循环会退出,轮训线程会终止
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break;
...
}
}
垃圾回收
还有一个特殊的场景需要特别注意,那就是当轮训线程因为队列里没有任务而睡眠的时候,Timer 对象因为不再被引用而被垃圾回收了。这时候需要主动唤醒轮训线程,让它退出。
class Timer {
...
private final Object threadReaper = new Object() {
@SuppressWarnings("deprecation")
protected void finalize() throws Throwable {
synchronized(queue) {
thread.newTasksMayBeScheduled = false;
queue.notify();
}
}
};
...
}