在日常 Java 开发中,我们经常需要用到定时任务,java.util.Timer 类就是一个常用的选择。但是,你是否真正了解 Timer 的底层实现原理?是否遇到过 Timer 带来的线程安全问题?或者任务执行时间过长导致后续任务延迟执行的问题?本文将深入 Timer 的源码,带你彻底搞懂它,并分享一些实战中的避坑经验。
Java学习之 Timer 源码详解
Timer 类是 Java 提供的一个简单的定时任务调度器,它允许你安排任务在未来的某个时间执行一次,或者定期重复执行。 要理解 Timer,我们需要关注它的几个核心组成部分:Timer 类本身、TimerTask 抽象类和底层的线程模型。
Timer 的线程模型
Timer 的核心是一个单线程模型。当你创建一个 Timer 对象时,它会创建一个后台线程(TimerThread)来执行所有安排的任务。这意味着所有的 TimerTask 都是在这个线程中顺序执行的。 如果某个任务执行时间过长,会阻塞后续任务的执行。这与基于线程池的 ScheduledExecutorService 形成了鲜明对比,后者可以使用多个线程并行执行任务。
public class Timer {
// 关联的 TimerThread,负责执行任务
private final TimerThread thread;
// 任务队列,存放待执行的任务
private final TaskQueue queue = new TaskQueue();
// 是否取消 Timer
private boolean cancelled = false;
public Timer() {
this("Timer-" + serialNumber()); // 给线程命名
}
Timer(String name) {
thread = new TimerThread(queue); // 创建 TimerThread
thread.setName(name); // 设置线程名称,方便排查问题
thread.start(); // 启动线程
}
}
TimerTask 的本质
TimerTask 是一个抽象类,你需要继承它并实现 run() 方法,这个方法包含了你想要定时执行的任务逻辑。
public abstract class TimerTask implements Runnable {
// 任务状态
int state = VIRGIN;
// 任务需要执行的时间
long nextExecutionTime;
// 执行周期,如果是一次性任务,则为 0
long period = 0;
// 任务执行体
public abstract void run();
}
Timer 的 schedule 方法分析
Timer 提供了多种 schedule 方法,用于安排任务的执行时间。其中最常用的几种包括:
schedule(TimerTask task, long delay):在 delay 毫秒后执行 task 一次。schedule(TimerTask task, Date time):在指定的时间 time 执行 task 一次。schedule(TimerTask task, long delay, long period):在 delay 毫秒后开始执行 task,以后每隔 period 毫秒重复执行一次。schedule(TimerTask task, Date firstTime, long period):在指定的时间 firstTime 开始执行 task,以后每隔 period 毫秒重复执行一次。
这些 schedule 方法最终都会将 TimerTask 对象添加到内部的任务队列 TaskQueue 中,并唤醒 TimerThread 线程。
public void schedule(TimerTask task, long delay) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
sched(task, System.currentTimeMillis()+delay, 0); // 调用 sched 方法
}
private void sched(TimerTask task, long time, long period) {
synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");
synchronized(task) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException("Task already scheduled or cancelled");
task.nextExecutionTime = time; // 设置下次执行时间
task.period = period; // 设置执行周期
queue.add(task); // 添加到任务队列
if (queue.getMin() == task) // 如果是队列中最早的任务,则唤醒线程
queue.notify(); // 唤醒 TimerThread
}
}
}
实战避坑经验
- 避免长时间阻塞的任务:由于
Timer使用单线程模型,长时间阻塞的任务会影响后续任务的执行。如果任务可能执行时间较长,建议使用ScheduledExecutorService,它基于线程池,可以并行执行任务。 - 处理异常:在
TimerTask的run()方法中,一定要捕获并处理所有可能抛出的异常。如果run()方法抛出未捕获的异常,Timer线程会终止,导致后续任务无法执行。 - 取消任务:不再需要的
TimerTask应该及时取消,防止资源浪费。可以使用TimerTask.cancel()方法取消任务,使用Timer.cancel()方法取消整个Timer。 - 注意线程安全:
Timer和TimerTask本身是线程安全的,但是run()方法中访问的共享资源需要进行适当的同步,避免并发问题。 - 时区问题:在使用
schedule(TimerTask task, Date time)方法时,要注意时区问题,确保指定的Date对象的时间是正确的。
代码示例:使用 Timer 调度任务
import java.util.Timer;
import java.util.TimerTask;
public class TimerExample {
public static void main(String[] args) {
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println("Task executed at: " + System.currentTimeMillis());
}
};
// 延迟 1 秒后执行任务
timer.schedule(task, 1000);
// 延迟 2 秒后执行任务,以后每隔 3 秒重复执行
// timer.schedule(task, 2000, 3000);
}
}
替代方案:ScheduledExecutorService
ScheduledExecutorService 是 Java 并发包中提供的更强大、更灵活的定时任务调度器。它基于线程池,可以并行执行任务,并提供了更多的调度选项,例如延迟执行、固定速率执行、固定延迟执行等。 在实际开发中,建议优先使用 ScheduledExecutorService。
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledExecutorServiceExample {
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); // 创建一个单线程的线程池
Runnable task = () -> {
System.out.println("Task executed at: " + System.currentTimeMillis());
};
// 延迟 1 秒后执行任务
executor.schedule(task, 1, TimeUnit.SECONDS);
// 延迟 2 秒后执行任务,以后每隔 3 秒重复执行
executor.scheduleAtFixedRate(task, 2, 3, TimeUnit.SECONDS);
// 注意:ExecutorService 的关闭需要谨慎处理,否则可能导致资源泄漏
// executor.shutdown(); // 平滑关闭
}
}
总结来说,java.util.Timer 是一个简单易用的定时任务调度器,但由于其单线程模型,在高并发、任务执行时间不确定的场景下,容易出现问题。建议在实际开发中,根据具体情况选择合适的定时任务调度器,例如 ScheduledExecutorService,并注意线程安全、异常处理等问题。深入理解 Java学习的 Timer 源码,能帮助我们更好地选择和使用这些工具。
冠军资讯