作为一名Java开发者,掌握多线程编程是进阶的必经之路。计时器(Timer)作为多线程编程中的经典案例,不仅能帮助我们理解线程调度机制,还能在实际开发中处理各种定时任务场景。今天我将结合自己多年的开发经验,带大家从标准库的使用到自定义实现,全面剖析Java计时器的原理与实现细节。
Java标准库中的Timer类位于java.util包中,它提供了一种简单的方式来安排任务在后台线程中执行。一个典型的Timer使用场景包括三个核心要素:
让我们看一个完整的示例代码:
java复制public static void main(String[] args) {
System.out.println("程序启动,将在4秒后输出LOVE");
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
System.out.println("LOVE");
}
}, 4000); // 4000毫秒=4秒后执行
}
注意:Timer是单线程执行的,如果一个任务执行时间过长,会影响后续任务的准时执行。在实际生产环境中,对于需要高精度定时或并发执行多个定时任务的场景,建议使用ScheduledThreadPoolExecutor。
标准库Timer的内部实现其实是一个"任务队列+工作线程"的组合:
schedule()方法时,任务会被放入一个优先级队列这种设计有几个关键特点:
理解了标准库Timer的工作原理后,我们可以尝试自己实现一个简化版的计时器。这个自定义计时器将包含以下核心组件:
我们使用PriorityBlockingQueue来存储待执行的任务,这是一个线程安全的优先级队列实现。选择它的原因有三:
java复制private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
这是一个用于线程间协调的同步对象,主要解决两个问题:
java复制private Object mailBox = new Object();
Task类封装了要执行的任务和它的执行时间,实现了Comparable接口以便优先级队列排序:
java复制static class Task implements Comparable<Task> {
private Runnable command; // 要执行的任务
private long time; // 绝对执行时间(毫秒时间戳)
public Task(Runnable command, long after) {
this.command = command;
// 将相对时间转换为绝对时间
this.time = System.currentTimeMillis() + after;
}
public void run() {
command.run();
}
@Override
public int compareTo(Task other) {
// 按执行时间排序,时间早的优先级高
return Long.compare(this.time, other.time);
}
}
实际经验:这里使用绝对时间而非相对时间存储,可以避免系统时间调整带来的问题。但在高精度要求的场景下,还需要考虑使用单调时钟(nanoTime)。
Worker是一个独立线程,负责从队列中取出任务并执行:
java复制class Worker extends Thread {
@Override
public void run() {
while (true) {
try {
Task task = queue.take();
long now = System.currentTimeMillis();
if (task.time > now) {
// 任务还未到执行时间
queue.put(task); // 重新放回队列
synchronized (mailBox) {
// 精确等待剩余时间
mailBox.wait(task.time - now);
}
} else {
// 执行任务
task.run();
}
} catch (InterruptedException e) {
// 被中断时退出循环
break;
}
}
}
}
构造函数负责初始化工作线程:
java复制public MyTimer() {
Worker worker = new Worker();
worker.start();
}
这是计时器的主要接口,用于安排新任务:
java复制public void schedule(Runnable command, long after) {
Task task = new Task(command, after);
queue.offer(task);
synchronized (mailBox) {
// 唤醒工作线程重新检查队列
mailBox.notify();
}
}
关键点:每次添加新任务后都需要notify(),因为新加入的任务可能是最早需要执行的,需要立即唤醒工作线程重新计算等待时间。
现在我们把所有部分组合起来,形成一个完整的自定义计时器实现:
java复制import java.util.concurrent.PriorityBlockingQueue;
public class MyTimer {
private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
private Object mailBox = new Object();
static class Task implements Comparable<Task> {
private Runnable command;
private long time;
public Task(Runnable command, long after) {
this.command = command;
this.time = System.currentTimeMillis() + after;
}
public void run() {
command.run();
}
@Override
public int compareTo(Task other) {
return Long.compare(this.time, other.time);
}
}
class Worker extends Thread {
@Override
public void run() {
while (true) {
try {
Task task = queue.take();
long now = System.currentTimeMillis();
if (task.time > now) {
queue.put(task);
synchronized (mailBox) {
mailBox.wait(task.time - now);
}
} else {
task.run();
}
} catch (InterruptedException e) {
break;
}
}
}
}
public MyTimer() {
Worker worker = new Worker();
worker.start();
}
public void schedule(Runnable command, long after) {
Task task = new Task(command, after);
queue.offer(task);
synchronized (mailBox) {
mailBox.notify();
}
}
}
测试代码示例:
java复制public static void main(String[] args) {
MyTimer timer = new MyTimer();
System.out.println("开始安排任务,当前时间:" + System.currentTimeMillis());
// 3秒后执行的任务
timer.schedule(() -> {
System.out.println("任务1执行,时间:" + System.currentTimeMillis());
}, 3000);
// 1秒后执行的任务
timer.schedule(() -> {
System.out.println("任务2执行,时间:" + System.currentTimeMillis());
}, 1000);
// 5秒后执行的任务
timer.schedule(() -> {
System.out.println("任务3执行,时间:" + System.currentTimeMillis());
}, 5000);
}
预期输出会按照任务安排的延迟时间顺序执行,即使代码中是乱序添加的。
虽然我们使用了PriorityBlockingQueue来保证队列操作的线程安全,但仍需注意:
任务执行异常:如果任务执行抛出未捕获异常,工作线程会终止
内存一致性:确保任务对象的字段对所有线程可见
空转问题:当前实现中,工作线程在任务未到时间时会重新放回队列,这可能导致不必要的上下文切换
定时精度:System.currentTimeMillis()精度有限,且受系统时间调整影响
任务取消:当前实现缺少任务取消机制
我们的自定义实现与标准库Timer有几个关键区别:
| 特性 | 自定义MyTimer | 标准库Timer |
|---|---|---|
| 线程模型 | 单工作线程 | 单工作线程 |
| 异常处理 | 未处理(需改进) | 终止Timer |
| 任务排序 | 基于优先级队列 | 基于优先级队列 |
| 系统时间敏感性 | 敏感(基于currentTimeMillis) | 敏感 |
| 任务取消支持 | 不支持(需扩展) | 支持(cancel()方法) |
对于生产环境,我有以下几点建议:
优先使用ScheduledThreadPoolExecutor:它提供了更强大的功能,包括:
考虑使用Quartz等专业调度框架:如果需要复杂的调度需求(如CRON表达式)
注意资源释放:Timer会创建非守护线程,如果不显式取消,可能导致应用无法正常退出
标准库Timer提供了scheduleAtFixedRate方法来实现固定速率调度。我们可以在自定义Timer中实现类似功能:
java复制public void scheduleAtFixedRate(Runnable command, long initialDelay, long period) {
Task task = new Task(() -> {
command.run();
// 重新安排下一次执行
scheduleAtFixedRate(command, period, period);
}, initialDelay);
queue.offer(task);
synchronized (mailBox) {
mailBox.notify();
}
}
为Task类添加取消标志:
java复制static class Task implements Comparable<Task> {
// ...原有字段...
private volatile boolean cancelled;
public void cancel() {
this.cancelled = true;
}
public void run() {
if (!cancelled) {
command.run();
}
}
}
然后在Worker中处理取消的任务:
java复制if (task.time > now) {
if (task.cancelled) {
continue; // 跳过已取消的任务
}
// ...原有逻辑...
}
当前实现是单线程执行任务,我们可以扩展为线程池执行:
java复制class Worker extends Thread {
private ExecutorService executor = Executors.newFixedThreadPool(4);
@Override
public void run() {
while (true) {
try {
Task task = queue.take();
long now = System.currentTimeMillis();
if (task.time > now) {
queue.put(task);
synchronized (mailBox) {
mailBox.wait(task.time - now);
}
} else {
executor.execute(task::run);
}
} catch (InterruptedException e) {
break;
}
}
}
}
通过这个自定义计时器的实现过程,我们不仅深入理解了Java多线程编程的核心概念,还掌握了如何设计一个实用的并发工具。这种从使用到实现的学习路径,是提升Java编程能力的有效方法。