Java Timer原理与自定义实现解析
在Java开发中,Timer常用于执行定时任务。本文将拆解Timer的工作原理,并指导读者如何通过PriorityBlockingQueue和Wait/Notify机制,从零构建一个具备优先级调度、低CPU占用且线程安全的自定义定时器,同时规避并发冲突陷阱。
标准库中的Timer机制
定时器(Timer)的本质是代替人工在指定时间点执行特定任务。Java标准库提供的java.util.Timer类提供了基础的定时功能。其核心方法schedule(TimerTask task, long delay)允许用户指定延迟时间(毫秒)后执行一个TimerTask。
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("定时器任务! ");
}
}, 1000);
}
上述代码创建了一个定时器实例,并提交了一个任务,使其在当前时间1秒后执行。虽然标准库使用方便,但理解其底层逻辑有助于在处理复杂调度需求时进行定制化开发。
自定义定时器的核心设计要素
要实现一个可靠的自定义定时器,需要解决三个核心问题:任务存储结构、执行线程调度以及时间判定的准确性。
第一步:设计任务对象与存储结构
提交的任务由“ Runnable执行体 ”和“ 实际执行时间 ”两部分组成。为了能够高效地获取“最早到期”的任务,需要使用支持优先级的数据结构。PriorityBlockingQueue是一个线程安全的无界阻塞队列,非常适合用于维护按时间排序的任务集合。
首先定义MyTask类来封装任务和触发时间:
class MyTask implements Comparable<MyTask> {
private final Runnable runnable;
private final long triggerTime; // 绝对执行时间戳
public MyTask(Runnable runnable, long triggerTime) {
this.runnable = runnable;
this.triggerTime = triggerTime;
}
public long getTriggerTime() {
return triggerTime;
}
public void run() {
runnable.run();
}
/**
* 根据触发时间排序,确保队头总是最早执行的任务
*/
@Override
public int compareTo(MyTask o) {
if (this.triggerTime < o.triggerTime) return -1;
else if (this.triggerTime == o.triggerTime) return 0;
else return 1;
}
}
接着构建定时器骨架,包含扫描线程和任务队列:
public class MyTimer {
private Thread scanThread;
private final PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
private final Object locker = new Object();
public void schedule(Runnable runnable, long delay) {
// 计算绝对触发时间
MyTask task = new MyTask(runnable, System.currentTimeMillis() + delay);
queue.put(task);
synchronized (locker) {
locker.notify();
}
}
// ... 其他逻辑
}
第二步:优化扫描线程的CPU消耗
简单的轮询方式会导致极高的CPU占用。例如,如果当前时间为8:00,而下一个任务在12:00执行,忙循环会浪费大量资源。正确的做法是采用“阻塞式”等待:wait()直到接近任务执行时间或收到新任务通知。
修改后的扫描线程逻辑如下:
private void initScanThread() {
scanThread = new Thread(() -> {
while (true) {
try {
synchronized (locker) {
// 1. 取出队头最早的任务
MyTask task = queue.take();
long now = System.currentTimeMillis();
// 2. 判断是否到达执行时间
if (now < task.getTriggerTime()) {
// 未到时间,放回队列,并阻塞等待剩余时间
queue.put(task); // 重新入堆以维持优先级顺序
long waitTime = task.getTriggerTime() - now;
if (waitTime > 0) {
locker.wait(waitTime);
} else {
// 等待期间已过,应继续执行
queue.put(task);
}
} else {
// 已到时间,释放锁执行任务
task.run();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
scanThread.start();
}
这里引入了locker.wait(time)。当任务未到期时,线程进入阻塞状态,系统不会分配CPU周期,直到超时或被notify唤醒。这种方式大幅降低了空闲时的CPU消耗。
第三步:解决并发竞态条件
在上述逻辑中,queue.take()与后续的wait()必须处于同一个锁保护范围内,否则会出现竞态条件。假设线程A在执行完take后被挂起,此时线程B插入了一个更早执行的任务并调用了notify。由于线程A尚未调用wait,这次唤醒将失效。当线程A恢复执行并调用wait(longTime)时,它将错误地等待一个极长的时间段,导致新插入的紧急任务被延误。
为确保原子性,synchronized (locker)块必须包裹从取任务到判断并调用的整个流程。同时,每次通过schedule插入新任务后调用locker.notify(),可以强制唤醒等待线程,使其重新评估队列中的最早任务是否需要提前执行。
总结
通过自定义定时器,我们不仅掌握了Java Timer的基本用法,更深入理解了并发编程中的阻塞等待与线程间通信。使用PriorityBlockingQueue管理任务顺序,配合wait/notify机制实现精准的时间调度,是处理此类问题的经典模式。以上就是Java定时器Timer原理与自定义实现的详细内容,更多关于Java多线程与定时任务的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













