背景与问题

在 Java 开发中,面对定时执行任务的需求,开发者往往会自然想到使用 TimerTimerTask。最近在使用这两个类实现定时任务时,发现了一个现象:当在 TimerTaskrun() 方法中使用 Thread.sleep() 时,定时器似乎失效了,后续任务无法按预期执行。

网上虽有类似问题的讨论,但缺乏深入的原理分析。为了解决这个困惑,本文通过阅读 JDK 源码,整理 TimerTimerTask 的内部实现机制,并分析上述问题的根本原因。

核心类职责

在 Java 中,与定时任务执行相关的核心类主要包括 TimerTimerTaskTimerThreadTaskQueue,它们的职责大致如下:

  • Timer:任务调度类。与 TimerTask 一样,它是暴露给最终用户使用的类,通过 schedule 方法安排任务的执行计划。该类内部通过 TaskQueueTimerThread 完成任务的调度。
  • TimerTask:实现 Runnable 接口。注意:虽然实现了 Runnable,但任务并非由独立线程执行,而是由 Timer 内部的单线程调度执行。该类提供一个重要的成员变量 nextExecutionTime,表示下一次执行该任务的时间。Timer 机制正是依靠这个值来安排任务执行顺序。
  • TimerThread:继承于 Thread,是真正执行任务的线程类。
  • TaskQueue:存储任务的数据结构,内部由最小堆实现。堆的每个节点为一个 TimerTask,每个任务依靠其 nextExecutionTime 值进行排序。也就是说,nextExecutionTime 最小的任务位于队列最前端,从而能够最早执行。

要想使用 Timer,用户只需要了解 TimerTimerTask。下面通过一个最基本的案例入手,来看一下 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() 恢复执行?因为这种方式已经满足所有的唤醒要求了:

  1. 如果安排当前 Task 之前 queue 为空,显然上述判断为 true,于是 mainLoop() 方法能够继续执行。
  2. 如果安排当前 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 替代,以获得更好的线程管理和异常处理能力。