Java ee初阶——定时器
一、定时器
1、定时器是什么
定时器也是软件开发中的一个重要组件,类似于一个"闹钟",达到一个设定的时间之后,就执行某个指定好的代码
定时器是一种实际开发中非常常用的组件。
比如网络通信中,如果对方500ms内没有返回数据,则断开连接尝试重连。
比如一⼀个Map,希望里面的某个key在3s之后过期(自动删除)。
类似于这样的场景就需要用到定时器。

2、标准库中的定时器
标准库中提供了一个Timer类,Timer类的核心方法为schedule
schedule包含两个参数,第一个参数指定即将要执行的任务代码,第⼆个参数指定多长时间之后执行(单位为毫秒)
Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello");}}, 3000);3、实现定时器
定时器的构成
一个带优先级队列(不要使用PriorityBlockingQueue,容易死锁!)
队列中的每个元素是一个Task对象
Task中带有一个时间属性,队首元素就是即将要执行的任务
同时有一个worker线程一直扫描队首元素,看队首元素是否需要执行
1)Timer类提供的核心接口为schedule,用于注册一个任务,并指定这个任务多长时间后执行
public class MyTimer {public void schedule(Runnable command, long after) {// TODO}}2)Task类用于描述⼀个任务(作为Timer的内部类),里面包含一个Runnable对象和一个time(毫秒时间戳)
这个对象需要放到优先队列中,因此需要实现 Comparable 接口
class MyTask implements Comparable<MyTask> {public Runnable runnable;// 为了⽅便后续判定, 使⽤绝对的时间戳. public long time;public MyTask(Runnable runnable, long delay) {this.runnable = runnable;// 取当前时刻的时间戳 + delay, 作为该任务实际执⾏的时间戳 this.time = System.currentTimeMillis() + delay;}@Overridepublic int compareTo(MyTask o) {// 这样的写法意味着每次取出的是时间最⼩的元素. // 到底是谁减谁?? 俺也记不住!!! 随便写⼀个, 执⾏下, 看看效果~~ return (int)(this.time - o.time);}}3)Timer实例中,通过PriorityQueue来组织若干个Task对象
通过schedule来往队列中插入一个个Task对象
class MyTimer {// 核⼼结构private PriorityQueue<MyTask> queue = new PriorityQueue<>();// 创建⼀个锁对象 private Object locker = new Object();public void schedule(Runnable command, long after) {// 根据参数, 构造 MyTask, 插⼊队列即可. synchronized (locker) {MyTask myTask = new MyTask(runnable, delay);queue.offer(myTask);locker.notify();} }}4)Timer类中存在一个worker线程,一直不停的扫描队首元素,看看是否能执行这个任务
所谓"能执行"指的是该任务设定的时间已经到达了
// 在这⾥构造线程, 负责执⾏具体任务了. public MyTimer() {Thread t = new Thread(() -> {while (true) {try {synchronized (locker) {// 阻塞队列, 只有阻塞的⼊队列和阻塞的出队列, 没有阻塞的查看队⾸元素. while (queue.isEmpty()) {locker.wait();}MyTask myTask = queue.peek();long curTime = System.currentTimeMillis();if (curTime >= myTask.time) {// 时间到了, 可以执⾏任务了 queue.poll();myTask.runnable.run();} else {// 时间还没到locker.wait(myTask.time - curTime);}}} catch (InterruptedException e) {e.printStackTrace();}}});t.start();}4. 为什么主线程不会被 “阻塞”?
因为 Timer 的调度是异步的(由独立线程执行),主线程在调用 timer.schedule 后会 “继续往下执行”,不会停在那里等 2 秒。
可以类比成:你(主线程)点了一份外卖(定时任务),然后继续做自己的事(打印 hello main);外卖小哥(Timer 线程)会在 2 秒后把外卖送到(执行 hello timer)。
Java 中 Timer 线程特性:
- 线程类型:Timer 内部包含前台线程(非守护线程)。
- 进程影响:前台线程会阻止 Java 进程结束,即使主线程执行完毕,只要 Timer 的前台线程还在运行,进程就会保持存活。
- 类比线程池:和线程池(若未显式配置守护线程)类似,都会因前台线程的存在,让进程无法自动终止,需主动关闭 Timer(如调用
timer.cancel())来释放资源。
模拟实现定时器
1. 任务类设计
创建一个类表示定时任务,需包含任务逻辑和执行时间(用于判断何时执行)。
2. 任务管理容器选择
- 若用
ArrayList管理多个任务,每次需遍历找到 “执行时间最早” 的任务,效率较低。 - 更优方案是使用优先级队列(
PriorityQueue),基于 “执行时间” 排序,让时间最早的任务始终在队首,可高效获取待执行任务。
3. schedule 方法实现
该方法负责将任务添加到优先级队列中,完成任务的注册。
4. 执行线程设计
额外创建一个独立线程,循环执行以下逻辑:
- 检查队首任务的执行时间是否到达;
- 若时间未到,线程等待(直到时间到达或被唤醒);
- 若时间到达,执行队首任务并将其从队列中移除。
定时器的线程安全问题,核心逻辑如下:
- 存在两个线程操作同一个队列:一个线程调用
schedule方法向队列中添加任务,另一个是定时器内部的线程从队列中获取并执行任务。 - 多线程同时操作同一个队列时,若没有合适的同步机制,会出现线程安全问题(如任务添加或获取时的竞态条件、数据不一致等)。
- 解决方法是对队列的操作(添加、获取任务)进行同步控制,例如使用
synchronized关键字、ReentrantLock等锁机制来保证线程安全。
计算任务的目标执行时间:任务的目标执行时间 = 任务提交时的系统时间 + 设定的延时时间(毫秒)
在 Lambda 表达式中,若涉及锁(如 synchronized 或 Lock),this 的指向始终是外部类的实例,而非 Lambda 表达式本身(因为 Lambda 表达式不是匿名内部类,它没有自己的 this)。
关键原因:Lambda 与匿名内部类的本质区别
- 匿名内部类:是一个独立的类实例,有自己的
this(指向匿名内部类实例),因此在匿名内部类中使用this时,指向的是该内部类自身。 - Lambda 表达式:是 “函数式接口的实例化简化”,它没有独立的
this,其内部的this与所在外部类的this完全一致(即指向外部类的当前实例)。
注意:在锁里的this 指向的是外面的类的
Lambda 表达式没有自己的 this。
在 Lambda 表达式内部使用 this 时,this 指向的是创建 Lambda 表达式的外部类的当前实例(即包围 Lambda 表达式的那个类的实例)。
原因:
Lambda 表达式本质是 “函数式接口的实例化简化”,但它不是一个独立的类(不同于匿名内部类),也不会生成独立的类对象。它更像是一个 “嵌入在外部类中的代码块”,因此不具备自身的 this 引用。
- Lambda 表达式内部的
this等价于外部类的this,指向外部类实例。 - Lambda 表达式自身没有独立的
this引用(因它不是独立的类实例)。
实现单线程的定时器,如果一口气来10000个任务一个一个的任务执行也太慢了,可以结合定时器来使用

定时任务的多线程执行架构设计思路,核心是通过 “分工协作” 的多线程模型来高效处理大量定时任务:
1. 线程角色分工
- 扫描线程:单独线程负责扫描定时任务队列,判断任务的执行时间是否到达。
- 执行线程池:由多个线程组成的线程池,负责实际执行到达时间的任务。
2. 流程逻辑
扫描线程持续监控任务队列,当检测到任务执行时间到达时,将该任务从定时队列中取出,提交到执行线程池的任务队列中;执行线程池中的多个线程并行从队列中获取任务并执行,从而实现任务的高效并发处理。
3. 场景适配性
针对 “一口气注册 100000 个 14:00 执行的任务” 这类大规模定时任务场景,该架构能有效解决单线程执行的性能瓶颈:
- 扫描线程只需专注于时间判断和任务分发,避免因任务执行耗时阻塞扫描逻辑;
- 执行线程池的多线程并行执行,可充分利用 CPU 资源,确保大量任务在时间点到达时能快速完成执行。
package thread;// 这样的写法基于抽象类的方式定义 MyTimerTask
// 这样的定义虽然确实可以, 写起来有点麻烦.
// 还有另外的写法.
//abstract class MyTimerTask implements Runnable {
// @Override
// public abstract void run();
//}import java.util.PriorityQueue;
import java.util.concurrent.Executors;class MyTimerTask implements Comparable<MyTimerTask> {private Runnable task;// 记录任务要执行的时刻private long time;public MyTimerTask(Runnable task, long time) {this.task = task;this.time = time;}@Overridepublic int compareTo(MyTimerTask o) {return (int) (this.time - o.time);// return (int) (o.time - this.time);}public long getTime() {return time;}public void run() {task.run();}
}// 自己实现一个定时器
class MyTimer {private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();// 直接使用 this 作为锁对象, 当然也是 ok 的private Object locker = new Object();public void schedule(Runnable task, long delay) {synchronized (locker) {// 以入队列这个时刻作为时间基准.MyTimerTask timerTask = new MyTimerTask(task, System.currentTimeMillis() + delay);queue.offer(timerTask);locker.notify();}}public MyTimer() {// 创建一个线程, 负责执行队列中的任务Thread t = new Thread(() -> {try {while (true) {synchronized (locker) {// 取出队首元素// 还是加上 whilewhile (queue.isEmpty()) {// 这里的 sleep 时间不好设定!!locker.wait();}MyTimerTask task = queue.peek();if (System.currentTimeMillis() < task.getTime()) {// 当前任务时间, 如果比系统时间大, 说明任务执行的时机未到locker.wait(task.getTime() - System.currentTimeMillis());} else {// 时间到了, 执行任务task.run();queue.poll();}}}} catch (InterruptedException e) {e.printStackTrace();}});t.start();}
}public class demo38 {public static void main(String[] args) {MyTimer timer = new MyTimer();timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello 3000");}}, 3000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello 2000");}}, 2000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello 1000");}}, 1000);Executors.newScheduledThreadPool(4);}
}
用 PriorityQueue 实现简易定时器(MyTimer)的例子,相当于自己实现了一个简化版的 java.util.Timer。
一、用 PriorityQueue 实现简易定时器(MyTimer)的例子,相当于自己实现了一个简化版的 java.util.Timer。
Timer timer = new Timer();
timer.schedule(task, delay);
但不用 Java 内置的 Timer 或 ScheduledExecutorService,而是自己实现时间调度机制。
1. MyTimerTask —— 封装任务与执行时间
class MyTimerTask implements Comparable<MyTimerTask> {private Runnable task; // 要执行的任务private long time; // 任务应该执行的绝对时间(单位:毫秒)public MyTimerTask(Runnable task, long time) {this.task = task;this.time = time;}public long getTime() { return time; }public void run() { task.run(); }@Overridepublic int compareTo(MyTimerTask o) {return (int)(this.time - o.time); // 时间早的任务排在前面}
}
作用:
把任务(
Runnable)和执行时间绑定起来;实现
Comparable接口,让PriorityQueue能自动按照时间排序。
2.MyTimer —— 定时器调度核心
class MyTimer {private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();private Object locker = new Object();public MyTimer() {Thread t = new Thread(() -> {try {while (true) {synchronized (locker) {while (queue.isEmpty()) {locker.wait();}MyTimerTask task = queue.peek();long current = System.currentTimeMillis();if (current < task.getTime()) {// 时间还没到,等待差值时间locker.wait(task.getTime() - current);} else {// 时间到了,执行并出队queue.poll().run();}}}} catch (InterruptedException e) {e.printStackTrace();}});t.start();}public void schedule(Runnable task, long delay) {synchronized (locker) {MyTimerTask timerTask = new MyTimerTask(task, System.currentTimeMillis() + delay);queue.offer(timerTask);locker.notify(); // 唤醒线程重新判断是否有更早的任务}}
}
逻辑解释:
一个后台线程持续工作(
while (true))。所有任务放进优先队列,时间早的排前面。
如果队列空 → 线程
wait()等待。如果时间没到 → 按剩余时间继续
wait(remainingTime)。如果时间到了 → 执行
run()并出队。
💡 使用 PriorityQueue 的好处:
自动保证最早的任务总在队首;
无需每次遍历整个队列找最早任务。
3.主程序测试
public class demo38 {public static void main(String[] args) {MyTimer timer = new MyTimer();timer.schedule(() -> System.out.println("hello 3000"), 3000);timer.schedule(() -> System.out.println("hello 2000"), 2000);timer.schedule(() -> System.out.println("hello 1000"), 1000);}
}


开始实现模拟定时器
1. 总体目标与约束
目标:实现一个简易定时器,支持 schedule(Runnable task, long delay),按时间顺序执行任务。
核心约束/设计选择(隐含假设):
使用一条调度线程来管理何时执行哪个任务(单调调度线程)。
使用
PriorityQueue存放待执行任务,按执行时间排序。线程间用一个对象锁与
wait()/notify()协调(最基础的同步手段)。任务执行通过调用
Runnable.run()(默认在调度线程里直接执行)。
2. MyTimerTask 的设计(封装任务与执行时间)
class MyTimerTask implements Comparable<MyTimerTask> {private Runnable task;private long time; // 绝对执行时间(毫秒)public MyTimerTask(Runnable task, long time) { ... }public long getTime() { return time; }public void run() { task.run(); }@Override public int compareTo(MyTimerTask o) { return (int)(this.time - o.time); }
}
为什么要这样封装?
把任务和时间绑定:
Runnable本身没有时间属性,包装成对象能把“任务”和“什么时候执行”绑定在一起,便于排序和管理。实现 Comparable:
PriorityQueue通过元素比较来决定队首元素。实现compareTo让队列始终能把最早执行的任务放在队首,便于只关注队首任务即可决定下一步动作。提供
run()方法:对外统一调用run(),便于未来替换实现或在调度线程外执行(若改成线程池执行只需改run()的调用位置)。
注意/改进点
compareTo内直接做(int)(this.time - o.time)会溢出,应使用Long.compare(this.time, o.time)。
原因:两个 long 相减再转 int 可能丢失或溢出,导致错排。time存绝对时间(System.currentTimeMillis()+ delay)还是相对延迟(delay)有差别:绝对时间便于比较、处理调度时差;
但对“时间回拨/系统时钟变化”敏感(见后文)。
可以扩展字段(id、cancelled flag、period 等)以支持取消、重复任务等功能。
3. 使用 PriorityQueue 存任务 —— 为什么是好选择
PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
设计动机
快速定位最早要执行的任务:调度逻辑只需看队首
peek()即可知道下一次要执行的任务,而无需遍历所有任务。插入操作复杂度较低:插入是 O(log n),适合不极端的大量任务场景。
语义清晰:优先队列天然表达“按时间优先”的语义。
风险与考虑
PriorityQueue不是线程安全 的 —— 必须外部加锁(本例用synchronized(locker))。PriorityQueue对重复时间、相等时间等行为是可接受的,但若需要稳定性(相同时间的先后顺序),可能需要额外字段(序号)保证一致性。如果任务量很大或需要高性能,可考虑更专业的数据结构(时间轮、HashedWheelTimer、heap 的并发版本等)。
4. 锁对象与同步:locker 和 synchronized
private Object locker = new Object();
synchronized (locker) { ... }
locker.wait(...);
locker.notify();
为什么要同步?
PriorityQueue不是线程安全的;schedule()由任意线程调用(外部线程),调度线程在后台访问队列。必须用同步保护队列的并发访问,避免数据结构破坏(race condition、heap 结构损坏)。wait()/notify()必须在同步块内调用(Java 语言要求),因此选择用locker作为监视器对象。
为什么不直接用 synchronized(this)?
可以用 this,但用独立的 locker 更明确、可替换(例如以后把锁替换为 ReentrantLock 时更方便),也可避免暴露锁给外部代码(若 this 被外部 synchronized 使用,容易增加耦合)。
5. 唤醒机制:为什么 schedule() 要 notify()?
timer.schedule(...) 插入任务后调用 locker.notify();
为什么需要唤醒?
调度线程可能正在
wait(remaining)—— 等待当前队首任务的到期时间。如果新插入的任务比当前队首任务更早(也就是需要更早执行),必须唤醒调度线程,让它重新计算应该等待多久(或者立刻执行新任务)。否则将错过更早任务的执行时刻。notify()的作用是唤醒等待线程,调度线程就会从wait()返回并重新检查队列和时间差。
notify() 还是 notifyAll()?
在当前设计(只有一个调度线程)
notify()足够且更高效。若未来有多个等待线程(比如为了扩展而启动多个调度线程),需要
notifyAll()或更复杂的协调机制。安全实践:当存在多种条件或多个等待者时,
notifyAll()更稳妥(避免死锁或遗漏唤醒),但代价是多线程被无谓唤醒。
6. 调度线程的核心循环
while (true) {synchronized (locker) {while (queue.isEmpty()) {locker.wait();}MyTimerTask task = queue.peek();long current = System.currentTimeMillis();if (current < task.getTime()) {locker.wait(task.getTime() - current);} else {queue.poll();task.run();}}
}
逐行解释并说明为什么这样写:
while (true):线程一直运行,负责不断调度任务。设计选择:常驻调度线程,直到被中断/关闭。替代:可增加退出条件(如
shutdown标志)来优雅终止线程。
synchronized (locker):在整个检查-等待-取出的过程中必须持锁,保证检查队列状态和wait/poll的原子性,防止竞态。while (queue.isEmpty()) locker.wait();使用
while而非if:防止虚假唤醒(Java 的wait()可被 spuriously wake up),以及在被唤醒后需要再次检查队列是否真有任务。如果用
if,虚假唤醒会导致线程继续执行,peek()可能返回 null,抛 NPE。
MyTimerTask task = queue.peek();— 只读取队首但不出队。为什么不直接
poll()?因为如果时间未到不能丢弃或提前执行;peek()允许检查时间再决定是否等待或执行。
if (current < task.getTime()) locker.wait(task.getTime() - current);用
wait(timeout)让线程在剩余时间内等待,减少 CPU 消耗(比 busy-wait 好)。当到期或被notify()(或被中断/虚假唤醒)返回后,循环会重新检查条件。使用
long差值要小心负数(若系统时间变动或计算误差),要确保传给wait的值非负(可Math.max(0, ...))。
else { queue.poll(); task.run(); }时间到后出队并执行。顺序:先
poll()再run()可避免任务在执行中再次被.peek到(线程安全)。但注意:如果
task.run()抛异常会中断调度线程的循环(若异常没有捕获)。因此在实际中应把task.run()包在try-catch内,防止一个任务崩掉整个调度线程。
为什么使用 wait(timeout) 而不是 sleep(timeout)?
sleep只会睡眠当前线程,不与锁/条件交互:如果使用sleep,在等待期间无法被notify()提前唤醒(只能被interrupt()打断)。那样当插入更早的任务时就无法及时调整等待时长,会导致延迟执行或复杂的轮询机制。wait(timeout)则在锁释放的同时进入等待状态,可接受notify()提前唤醒并重新判断。
7. 关于时间来源:System.currentTimeMillis() vs System.nanoTime()
代码中用 System.currentTimeMillis() + delay 计算绝对执行时刻。
问:为什么用 currentTimeMillis()?有风险吗?
currentTimeMillis()返回墙钟时间(wall-clock),会受系统时间调整(NTP 校正、人工修改、夏令时等)影响。若系统时钟向后调整,可能产生长时间等待或错过任务;向前调整则可能导致任务提前执行。更稳健的做法:对于代表时间间隔的比较(等待多少毫秒),建议使用
System.nanoTime()(单调递增,用于测量间隔,不受系统时钟调整影响)。计算方式:记录deadlineNano = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(delay),比较nanoTime()与deadlineNano。
结论/建议:
如果你只关心“过了多少时间后执行”(延迟),用
nanoTime更安全。如果你想在特定的“真实时刻”执行(例如“某日某点”),需要
currentTimeMillis()。
8. 为什么用单调调度线程(调度线程也执行任务)?利与弊
优点:
实现简单:调度线程直接
task.run(),无需线程池或任务转发。资源少:不创建额外线程(除调度线程外)。
缺点:
如果某个任务执行时间很长,会阻塞后续任务(调度延迟)。
不利于并发执行多个同一时刻的任务。
无法利用多核并行执行任务。
改进:
把任务提交给
ExecutorService(线程池)执行:调度线程只负责时间判断并executor.submit(task)。这样调度线程不会被单个任务阻塞;但需要处理任务提交失败或线程池饱和的情况。例如:
MyTimerTask t = queue.poll();
executor.execute(() -> {try { t.run(); } catch (Throwable e) { ... }
});
构造方法启动线程是为了:
- 初始化时就准备好“定时器引擎”(只启动一次)
- 避免每次 schedule 都重复创建线程
- 实现“单例后台线程”模型,所有任务共用一个执行线程
- 符合 Timer 的语义:创建即运行,随时可以调度任务
定时器用线程池实现
package thread;import java.util.PriorityQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;class MyTimerTask implements Comparable<MyTimerTask> {private Runnable task;private long time;private volatile boolean cancelled = false;public MyTimerTask(Runnable task, long time) {this.task = task;this.time = time;}@Overridepublic int compareTo(MyTimerTask o) {return Long.compare(this.time, o.time);}public long getTime() {return time;}public void run() {if (!cancelled) {task.run();}}public void cancel() {cancelled = true;}public boolean isCancelled() {return cancelled;}
}// 模拟实现的定时器 - 使用线程池但保持原有逻辑
class MyTimer {private final PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();private final Object locker = new Object();private final ExecutorService workerPool;private final Thread scannerThread;private volatile boolean running = true;// 自定义线程工厂,给线程命名private static class TimerThreadFactory implements ThreadFactory {private final AtomicInteger threadNumber = new AtomicInteger(1);private final String namePrefix;TimerThreadFactory(String poolName) {namePrefix = poolName + "-thread-";}@Overridepublic Thread newThread(Runnable r) {Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement());t.setDaemon(true);return t;}}public MyTimer() {this(1); // 默认使用1个工作线程}public MyTimer(int workerThreads) {// 创建工作线程池workerPool = Executors.newFixedThreadPool(workerThreads, new TimerThreadFactory("timer-worker"));// 创建扫描线程(保持原有设计)scannerThread = new Thread(() -> {try {scanAndExecute();} catch (InterruptedException e) {System.out.println("扫描线程被中断");}}, "timer-scanner");scannerThread.setDaemon(true);scannerThread.start();}public void schedule(Runnable task, long delay) {if (delay < 0) {throw new IllegalArgumentException("延迟时间不能为负数");}MyTimerTask timerTask = new MyTimerTask(task, System.currentTimeMillis() + delay);synchronized (locker) {queue.offer(timerTask);locker.notify(); // 唤醒扫描线程System.out.println(Thread.currentThread().getName() + " - 添加任务,延迟: " + delay + "ms, 队列大小: " + queue.size());}}// 取消所有任务public void cancel() {synchronized (locker) {for (MyTimerTask task : queue) {task.cancel();}queue.clear();locker.notify();}}// 停止定时器public void stop() {running = false;workerPool.shutdownNow();scannerThread.interrupt();synchronized (locker) {locker.notifyAll();}}// 获取待执行任务数量public int getTaskCount() {synchronized (locker) {return queue.size();}}// 核心扫描和执行逻辑private void scanAndExecute() throws InterruptedException {while (running && !Thread.currentThread().isInterrupted()) {synchronized (locker) {// 等待队列不为空while (queue.isEmpty() && running) {locker.wait();}if (!running) {break;}// 检查队首任务MyTimerTask task = queue.peek();if (task == null) {continue;}long currentTime = System.currentTimeMillis();long taskTime = task.getTime();long waitTime = taskTime - currentTime;if (waitTime <= 0) {// 任务时间已到,从队列移除queue.poll();if (!task.isCancelled()) {// 使用线程池执行任务,而不是直接在当前线程执行System.out.println(Thread.currentThread().getName() + " - 提交任务到线程池执行");workerPool.execute(() -> {System.out.println(Thread.currentThread().getName() + " - 开始执行任务");task.run();System.out.println(Thread.currentThread().getName() + " - 任务执行完成");});}} else {// 等待到任务执行时间System.out.println(Thread.currentThread().getName() + " - 等待 " + waitTime + "ms");locker.wait(waitTime);}}}System.out.println("扫描线程退出");}
}// 测试类
public class demo38 {public static void main(String[] args) throws InterruptedException {System.out.println("=== 模拟实现定时器(线程池版)===");// 创建定时器,使用2个工作线程MyTimer timer = new MyTimer(2);// 添加多个任务for (int i = 1; i <= 5; i++) {final int taskId = i;timer.schedule(() -> {System.out.println(Thread.currentThread().getName() + " - 任务" + taskId + " 执行");try {Thread.sleep(1000); // 模拟任务执行时间} catch (InterruptedException e) {Thread.currentThread().interrupt();}}, i * 1000L); // 1秒, 2秒, 3秒...}// 添加一个长时间任务timer.schedule(() -> {System.out.println(Thread.currentThread().getName() + " - 长时间任务开始");try {Thread.sleep(3000);} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.println(Thread.currentThread().getName() + " - 长时间任务结束");}, 6000);// 监控队列大小Thread monitorThread = new Thread(() -> {try {for (int i = 0; i < 10; i++) {System.out.println("监控 - 待执行任务数: " + timer.getTaskCount());Thread.sleep(1000);}} catch (InterruptedException e) {Thread.currentThread().interrupt();}}, "monitor-thread");monitorThread.start();// 等待所有任务执行Thread.sleep(15000);System.out.println("\n=== 测试任务取消 ===");// 测试取消功能MyTimer timer2 = new MyTimer(1);// 添加一些任务for (int i = 1; i <= 3; i++) {final int taskId = i;timer2.schedule(() -> {System.out.println("定时器2 - 任务" + taskId + " 执行");}, i * 2000L);}Thread.sleep(2500);// 取消剩余任务System.out.println("取消前任务数: " + timer2.getTaskCount());timer2.cancel();System.out.println("取消后任务数: " + timer2.getTaskCount());Thread.sleep(2000);// 停止定时器timer.stop();timer2.stop();System.out.println("测试完成");}
}时间轮定时器
1. 数据结构设计
- 循环数组(时间轮主体):将时间划分为多个 “时间单位”,用循环数组存储这些时间单位,数组的每个元素对应一个时间单位。
- 链表(任务容器):数组的每个元素关联一个链表,用于存储该时间单位内需要执行的定时任务。
2. 执行逻辑
- 定时器维护一个 “光标”,每经过一个时间单位,光标就移动到数组的下一个元素(循环移动)。
- 当光标移动到某个数组元素时,就遍历该元素对应的链表,执行所有到期的任务。
3. 与基于堆的定时器对比
- 基于堆的定时器:通过优先级队列(堆)按任务执行时间排序,每次取出最早到期的任务执行,适用于任务数量不多的场景。
- 时间轮定时器:通过数组 + 链表的结构,将任务按时间单位分组,可高效处理大量定时任务(尤其是时间粒度相对固定的场景),时间复杂度更优。
时间轮适合时间精度高的定时器,优先级队列实现的定时器执行任务多的
一、时间轮定时器实现(高效处理大量任务)
时间轮通过 “循环数组 + 链表” 按时间单位分组管理任务,适合时间粒度固定(如 10ms/100ms)、任务数量庞大的场景。
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;/*** 时间轮定时器* 特点:按固定时间粒度(tickMs)分组任务,高效处理大量定时任务,时间精度由tickMs决定*/
public class TimeWheelTimer {// 时间轮的时间粒度(每个格子代表的毫秒数,决定精度)private final long tickMs;// 时间轮的格子数量(循环数组大小)private final int wheelSize;// 时间轮总覆盖时间范围(tickMs * wheelSize)private final long totalRangeMs;// 循环数组:每个格子对应一个时间单位,存储该单位内的任务链表private final List<TimerTask>[] buckets;// 当前指针位置(指向当前时间单位对应的格子)private int currentIndex;// 当前时间轮的绝对时间(毫秒)private long currentTime;// 工作线程:推动时间轮前进并执行任务private final Thread workerThread;// 锁:保证线程安全private final ReentrantLock lock = new ReentrantLock();// 标记是否运行private volatile boolean isRunning;// 定时任务封装static class TimerTask {Runnable task; // 实际任务long delayMs; // 延迟时间(用户传入)long targetTime; // 目标执行时间(绝对时间)public TimerTask(Runnable task, long delayMs, long targetTime) {this.task = task;this.delayMs = delayMs;this.targetTime = targetTime;}}/*** 初始化时间轮* @param tickMs 时间粒度(毫秒,如100ms,精度越高性能消耗略大)* @param wheelSize 格子数量(总范围 = tickMs * wheelSize)*/public TimeWheelTimer(long tickMs, int wheelSize) {this.tickMs = tickMs;this.wheelSize = wheelSize;this.totalRangeMs = tickMs * wheelSize;// 初始化循环数组(每个格子是一个任务链表)this.buckets = new List[wheelSize];for (int i = 0; i < wheelSize; i++) {buckets[i] = new LinkedList<>();}this.currentTime = System.currentTimeMillis();this.currentIndex = 0;this.isRunning = true;// 启动工作线程this.workerThread = new Thread(this::run, "TimeWheel-Worker");this.workerThread.start();}// 添加定时任务public void schedule(Runnable task, long delayMs) {if (delayMs < 0) {throw new IllegalArgumentException("延迟时间不能为负数");}long targetTime = System.currentTimeMillis() + delayMs;TimerTask timerTask = new TimerTask(task, delayMs, targetTime);lock.lock();try {// 计算任务应放入的格子索引int bucketIndex = calculateBucketIndex(targetTime);buckets[bucketIndex].add(timerTask);} finally {lock.unlock();}}// 计算任务所在的格子索引private int calculateBucketIndex(long targetTime) {// 计算与当前时间的差值long offset = targetTime - currentTime;// 若超过总范围,取模(简化处理,多层时间轮可优化)if (offset >= totalRangeMs) {offset %= totalRangeMs;}// 计算索引(当前指针 + 偏移的格子数)% 总格子数return (currentIndex + (int) (offset / tickMs)) % wheelSize;}// 工作线程逻辑:推动时间轮private void run() {while (isRunning) {try {// 休眠一个时间粒度,推动时间轮前进Thread.sleep(tickMs);lock.lock();try {currentTime += tickMs; // 更新当前时间currentIndex = (currentIndex + 1) % wheelSize; // 移动指针// 取出当前格子的所有任务List<TimerTask> currentBucket = buckets[currentIndex];if (!currentBucket.isEmpty()) {// 执行所有任务(用新线程执行,避免阻塞时间轮)for (TimerTask task : currentBucket) {new Thread(task.task, "Task-Executor").start();}currentBucket.clear(); // 清空当前格子}} finally {lock.unlock();}} catch (InterruptedException e) {if (!isRunning) {break;}}}}// 停止定时器public void stop() {isRunning = false;workerThread.interrupt();}// 测试public static void main(String[] args) throws InterruptedException {// 创建时间轮:100ms精度,100个格子(总覆盖10秒)TimeWheelTimer timer = new TimeWheelTimer(100, 100);// 添加1000个任务(模拟大量任务)for (int i = 0; i < 1000; i++) {final int num = i;// 任务延迟1-3秒随机分布long delay = 1000 + new Random().nextInt(2000);timer.schedule(() -> System.out.println("任务" + num + "执行,延迟" + delay + "ms"), delay);}// 运行5秒后停止Thread.sleep(5000);timer.stop();}
}二、基于堆(优先级队列)的定时器实现(适合任务量不大的场景)
基于优先级队列(小顶堆)按任务执行时间排序,每次取出最早到期的任务执行,实现简单但大量任务时性能略低。
import java.util.PriorityQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;/*** 基于堆(优先级队列)的定时器* 特点:实现简单,适合任务数量不多的场景,时间精度高(依赖系统时间)*/
public class HeapBasedTimer {// 优先级队列(小顶堆):按任务执行时间升序排序private final PriorityQueue<TimerTask> taskQueue;// 锁和条件变量:保证线程安全和等待唤醒private final ReentrantLock lock = new ReentrantLock();private final Condition condition = lock.newCondition();// 工作线程:执行到期任务private final Thread workerThread;// 标记是否运行private volatile boolean isRunning;// 定时任务封装(实现Comparable按执行时间排序)static class TimerTask implements Comparable<TimerTask> {Runnable task; // 实际任务long targetTime; // 目标执行时间(绝对时间)public TimerTask(Runnable task, long targetTime) {this.task = task;this.targetTime = targetTime;}@Overridepublic int compareTo(TimerTask o) {// 按目标时间升序排序(小顶堆)return Long.compare(this.targetTime, o.targetTime);}}public HeapBasedTimer() {this.taskQueue = new PriorityQueue<>();this.isRunning = true;// 启动工作线程this.workerThread = new Thread(this::run, "HeapTimer-Worker");this.workerThread.start();}// 添加定时任务public void schedule(Runnable task, long delayMs) {if (delayMs < 0) {throw new IllegalArgumentException("延迟时间不能为负数");}long targetTime = System.currentTimeMillis() + delayMs;TimerTask timerTask = new TimerTask(task, targetTime);lock.lock();try {taskQueue.add(timerTask);condition.signal(); // 唤醒等待的工作线程} finally {lock.unlock();}}// 工作线程逻辑:循环执行最早到期的任务private void run() {while (isRunning) {lock.lock();try {// 循环检查队列是否为空while (taskQueue.isEmpty()) {condition.await(); // 空队列时等待}// 取出最早到期的任务TimerTask task = taskQueue.peek();long currentTime = System.currentTimeMillis();if (currentTime < task.targetTime) {// 未到期,等待剩余时间condition.await(task.targetTime - currentTime);} else {// 到期,执行任务并移除taskQueue.poll();new Thread(task.task, "Task-Executor").start();}} catch (InterruptedException e) {if (!isRunning) {break;}} finally {lock.unlock();}}}// 停止定时器public void stop() {isRunning = false;workerThread.interrupt();}// 测试public static void main(String[] args) throws InterruptedException {HeapBasedTimer timer = new HeapBasedTimer();// 添加少量任务(适合该定时器的场景)timer.schedule(() -> System.out.println("任务1执行(延迟1000ms)"), 1000);timer.schedule(() -> System.out.println("任务2执行(延迟2000ms)"), 2000);timer.schedule(() -> System.out.println("任务3执行(延迟500ms)"), 500);// 运行3秒后停止Thread.sleep(3000);timer.stop();}
}
总结
- 时间轮定时器:适合高并发场景,尤其是任务数量庞大且时间粒度相对固定的情况(如分布式系统中的定时任务),通过分组批量处理任务提升效率。
- 基于堆的定时器:实现简单,适合任务量不大、需要灵活处理任意延迟时间的场景(如单机小量定时任务),但大量任务时性能会下降。

