Java Timer&TimerTask原理分析
背景与问题
在 Java 开发中,面对定时执行任务的需求,开发者往往会自然想到使用 Timer 和 TimerTask。最近在使用这两个类实现定时任务时,发现了一个现象:当在 TimerTask 的 run() 方法中使用 Thread.sleep() 时,定时器似乎失效了,后续任务无法按预期执行。
网上虽有类似问题的讨论,但缺乏深入的原理分析。为了解决这个困惑,本文通过阅读 JDK 源码,整理 Timer 与 TimerTask 的内部实现机制,并分析上述问题的根本原因。
核心类职责
在 Java 中,与定时任务执行相关的核心类主要包括 Timer、TimerTask、TimerThread 和 TaskQueue,它们的职责大致如下:
- Timer:任务调度类。与
TimerTask一样,它是暴露给最终用户使用的类,通过schedule方法安排任务的执行计划。该类内部通过TaskQueue和TimerThread完成任务的调度。 - TimerTask:实现
Runnable接口。注意:虽然实现了Runnable,但任务并非由独立线程执行,而是由Timer内部的单线程调度执行。该类提供一个重要的成员变量nextExecutionTime,表示下一次执行该任务的时间。Timer机制正是依靠这个值来安排任务执行顺序。 - TimerThread:继承于
Thread,是真正执行任务的线程类。 - TaskQueue:存储任务的数据结构,内部由最小堆实现。堆的每个节点为一个
TimerTask,每个任务依靠其nextExecutionTime值进行排序。也就是说,nextExecutionTime最小的任务位于队列最前端,从而能够最早执行。
要想使用 Timer,用户只需要了解 Timer 和 TimerTask。下面通过一个最基本的案例入手,来看一下 Timer 内部的实现原理。
使用示例
import java.util.Timer;
import java.util.TimerTask;
import org.junit.Test;
class TestTimerTask extends TimerTask {
@Override
public void run() {
System.out.println("TestTimerTask is running......");
}
}
public class TimerTaskTest {
@Test
public void testTimerTask() {
Timer timer = new Timer();
// 延迟 0ms 执行,之后每 1000ms 执行一次
timer.schedule(new TestTimerTask(), 0, 1000);
}
}上面的代码是一个典型的 Timer & TimerTask 应用。下面先来看一下 new Timer() 做了什么。
源码分析
Timer 初始化
创建 Timer 对象的源码如下:
public Timer(String name) {
thread.setName(name); // thread 为 TimerThread 实例
thread.start();
}从源代码可知,创建 Timer 对象的同时也启动了 TimerThread 线程。
TimerThread 执行循环
接下来看看 TimerThread 做了什么:
public void run() {
try {
mainLoop(); // 线程真正执行的代码在这个私有方法中
} finally {
// Someone killed this Thread, behave as if Timer cancelled
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}接着来看私有方法 mainLoop() 的核心逻辑:
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired; // 是否已经到达 Task 的执行时间
synchronized(queue) {
// Wait for queue to become non-empty
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait(); // Timer 通过 wait & notify 方法安排线程之间的同步
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die
// Queue nonempty; look at first evt and do the right thing
long currentTime, executionTime;
task = queue.getMin();
synchronized(task.lock) {
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
// Task 的执行时间已到,设置 taskFired 为 true
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);
}
}
}
// 还没有执行时间,通过 wait 等待特定时间
if (!taskFired)
queue.wait(executionTime - currentTime);
}
// 已经到达执行时间,执行任务(不持有锁)
if (taskFired)
task.run();
} catch(InterruptedException e) {
}
}
}也就是说,一旦创建了 Timer 类的实例,就会存在一个循环遍历 queue 中的任务。如果有任务且时间到达,就通过线程去执行该任务;否则线程通过 wait() 方法阻塞自己。由于没有任务在队列中时没有必要继续循环,线程会进入等待状态。
任务调度 (schedule)
上面提到,如果 Timer 的任务队列中不包含任务时,TimerThread 线程并不会执行。接着来看看为 Timer 添加任务后会出现怎样的情况。
为 Timer 添加任务就是 timer.schedule() 做的事。schedule() 方法直接调用 Timer 的私有方法 sched(),sched() 是真正安排 Task 的地方,其源代码如下:
private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");
synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");
synchronized(task.lock) {
// 我喜欢 virgin 状态,其他状态表明该 Task 已经被 schedule 过了
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
// 设置 Task 下一次应该执行的时间,由 System.currentTimeMillis()+/-delay 得到
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}
// queue 为 TaskQueue 类的实例,添加任务到队列中
queue.add(task);
// 获取队列中 nextExecutionTime 最小的任务,如果与当前任务相同
if (queue.getMin() == task)
queue.notify(); // 还记得前面看到的 queue.wait() 方法么
}
}为什么要判断 queue.getMin() == task 时才通过 queue.notify() 恢复执行?因为这种方式已经满足所有的唤醒要求了:
- 如果安排当前 Task 之前
queue为空,显然上述判断为true,于是mainLoop()方法能够继续执行。 - 如果安排当前 Task 之前
queue不为空,那么mainLoop()方法不会一直被阻塞,不需要notify方法调用。
调用该方法还有一个好处:如果当前安排的 Task 的下一次执行时间比 queue 中其余 Task 的下一次执行时间都要小,通过 notify 方法可以提前打开 queue.wait(executionTime - currentTime) 方法对 mainLoop() 造成的阻塞,从而使得当前任务能够被优先执行,有点“抢占”的味道。
问题根源与总结
上述分析可以看出,Java 中 Timer 机制的实现仅仅使用了 JDK 中的基础方法,通过 wait & notify 机制实现。其源代码虽然简单,但这种实现机制会对开发者造成一种困扰。
从 sched() 方法和 mainLoop() 可以看出,Timer 内部维护的是单线程(TimerThread)。对于一个重复执行的任务,Timer 的实现机制是先安排 Task 下一次执行的时间,然后再启动 Task 的执行。
回到最初的问题: 为什么在 run() 方法中使用 Thread.sleep() 会导致 Timer 失效?
因为 Timer 只有一个后台线程负责执行所有任务。如果某个任务的 run() 方法中执行了耗时操作(如 Thread.sleep()),该线程会被阻塞,导致队列中后续的任务无法被调度执行,直到当前任务结束。这就是“单线程串行执行”带来的限制。
了解了 Timer 的实现原理,也就明白了其适用场景:适用于任务执行时间短、对实时性要求不高的场景。若任务可能耗时较长或需要并发执行,建议考虑其他方案。
说明:本文基于 JDK 经典Timer实现分析。在现代 Java 开发中,Timer已被视为遗留类(Legacy),推荐使用ScheduledThreadPoolExecutor替代,以获得更好的线程管理和异常处理能力。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/java-timertimertask-yuan-li-fen-xi.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。