Java Timer 定时调度器实现原理
使用 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;
}
任意线程都可以通过 Timer.schedule 方法将任务加入 TaskQueue,但是 TaskQueue 又并不是线程安全的数据结构。所在每次修改 TaskQueue 时都需要加锁。
synchronized(queue) {
...
}