手写计时器是怎么跑起来的:优先队列 + 单工作线程 + wait/notify

这段代码的核心思想非常“工程化”:把未来要执行的任务按时间排序存起来,然后开一个专门的线程盯着“下一件该做的事”,时间没到就睡觉,时间到了就执行。


源代码(已纠错版)

package thread;
//基于抽象类的方式定义MyTimerTask
//abstract class MyTimerTask implements Runnable{
//    @Override
//    public abstract void run();
//}

import java.util.PriorityQueue;
import java.util.Timer;
import java.util.TimerTask;

class MyTimerTask implements Comparable<MyTimerTask>{
    private Runnable task;
    //记录任务要执行的时刻
    private long time;
    public MyTimerTask(Runnable task,long time){
        this.task = task;
        this.time = time;
    }

    public int compareTo(MyTimerTask o){
        return (int) (this.time - o.time);
    }
    public long getTime(){
        return this.time;
    }
    public void run(){
        task.run();
    }
}
//自己实现一个定时器
class MyTimer{
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    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) {
                       //取出队首元素
                       //对于wait,保险起见还是用while
                       while (queue.isEmpty()) {
                           //continue;
                           locker.wait();
                       }
                       MyTimerTask task = queue.peek();
                       if (System.currentTimeMillis() < task.getTime()) {
                           //当前任务时间,如果比系统时间大,说明任务执行时机未到
                           //continue;用continue就一直等着占资源
                           locker.wait(task.getTime() - System.currentTimeMillis());
                       } else {
                           //时间到了,执行任务
                           task.run();
                           queue.poll();
                       }
                   }
               }
           }catch (InterruptedException e){
               throw new RuntimeException();
           }
        });
        t.start();
    }
}

public class 实现定时器 {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        //和线程池一样,Timer也包含前台线程,阻止进程结束
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("Hello,Timer2000");
            }
        },2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("Hello,Timer1000");
            }
        },1000);
        System.out.println("Hello main");
    }
}

1)MyTimerTask:把“任务”和“执行时刻”绑在一起

class MyTimerTask{
    private Runnable task;
    private long time; // 任务应执行的绝对时间点(毫秒时间戳)
}
  • Runnable task:真正要执行的逻辑(相当于“回调函数”)。
  • long time:这个任务应该在什么时候执行(用 System.currentTimeMillis() 的时间戳表示)。

schedule(task, delay) 会把 “delay 毫秒后执行” 转成 “在 now + delay 这个绝对时刻执行”。


2)PriorityQueue:让“最近要执行的任务”永远在队首

private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

PriorityQueue 是小根堆(最小值优先)。只要比较规则按 time 排序:

  • peek() 取出的永远是最早要执行的任务
  • poll() 弹出的也是最早要执行的任务

这就实现了计时器最关键的功能:无论提交顺序如何,都按触发时间先后执行


3)schedule:提交一个“未来任务”,顺便叫醒工作线程

public void schedule(Runnable task,long delay){
    synchronized (locker) {
        MyTimerTask timerTask = new MyTimerTask(task, System.currentTimeMillis() + delay);
        queue.offer(timerTask);
        locker.notify();
    }
}

这里做了三件事:

  1. 加锁:保护 queue(多个线程同时 schedule 时必须线程安全)。
  2. 计算触发时间now + delay
  3. 入队:放入优先队列。
  4. notify 唤醒:如果工作线程正在等待(队列空 / 或者在做超时等待),叫醒它重新计算“下一件事什么时候执行”。

4)计时器线程:一直盯着队首任务,没到点就 wait(剩余时间)

构造函数里创建了一个线程,线程逻辑是计时器的心脏:

while (true) {
    synchronized (locker) {
        while (queue.isEmpty()) locker.wait();

        MyTimerTask task = queue.peek();
        if (System.currentTimeMillis() < task.getTime()) {
            locker.wait(task.getTime() - System.currentTimeMillis());
        } else {
            task.run();
            queue.poll();
        }
    }
}

关键点拆解

A. 队列空:无限期等待

while (queue.isEmpty()) locker.wait();
  • 没有任务就睡到有人 schedulenotify
  • while 不用 if:防止虚假唤醒/竞争唤醒后条件又不成立。

B. 队列不空:看队首任务什么时候执行

MyTimerTask task = queue.peek();

队首就是“下一件最早要做的事”。

C. 时间没到:带超时等待

locker.wait(task.getTime() - now);

这一步特别关键:不是 busy loop 空转,而是让线程“睡到差不多该醒的时刻”,节省 CPU。

D. 时间到了:执行任务并出队

task.run();
queue.poll();

5)用一个例子理解执行顺序

假设提交两个任务:

  • A:延迟 2000ms
  • B:延迟 1000ms

优先队列会让 B 的 time 更小,所以 B 在队首:

  1. 工作线程 peek() 得到 B,发现没到点 → wait(剩余时间)
  2. 到点醒来执行 B,poll() 把 B 移除
  3. peek() 得到 A,继续等待到 A 的时间点执行

结果一定是:1000ms 的先执行,2000ms 的后执行,即使提交顺序反过来也一样。


6)这段实现里有哪些明显问题(不修就跑不起来/容易出坑)

问题 1:MyTimerTask 没有实现 ComparablePriorityQueue 可能无法排序

现在只有 compareTo 方法,但类声明里缺少:

class MyTimerTask implements Comparable<MyTimerTask> { ... }

并且比较最好别强转 int,可能溢出,建议:

@Override
public int compareTo(MyTimerTask o) {
    return Long.compare(this.time, o.time);
}

问题 2:工作线程创建了但没有 start(),计时器根本不会运行

构造函数里:

Thread t = new Thread(() -> { ... });

缺少:

t.start();

没有 start,线程永远不会执行那段 while(true) 的调度逻辑。

问题 3:在 synchronized(locker) 里直接 task.run(),会拖死 schedule

当前代码在持锁状态下运行任务:

synchronized(locker) {
    task.run(); // 这里执行任务
}

如果任务执行很慢,其他线程 schedule() 想往队列加任务会一直阻塞在锁上,吞吐很差。

更合理的结构是:锁内只做队列操作,锁外执行任务(先 poll 出来再跑)。

问题 4:main 里用的是 Timer,不是 MyTimer

main 里演示的是 JDK 的 java.util.Timer,并没有实际调用自写的 MyTimer。要演示自写计时器,应类似:

MyTimer timer = new MyTimer();
timer.schedule(() -> System.out.println("Hello 2000"), 2000);
timer.schedule(() -> System.out.println("Hello 1000"), 1000);
System.out.println("Hello main");

7)给一份“能正确跑、并且更像标准实现”的最小修正版

import java.util.PriorityQueue;

class MyTimerTask implements Comparable<MyTimerTask> {
    private final Runnable task;
    private final long time;

    public MyTimerTask(Runnable task, long time) {
        this.task = task;
        this.time = time;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        return Long.compare(this.time, o.time);
    }

    public long getTime() { return time; }
    public void run() { task.run(); }
}

class MyTimer {
    private final PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    private final Object locker = new Object();

    public MyTimer() {
        Thread t = new Thread(() -> {
            try {
                while (true) {
                    MyTimerTask taskToRun;

                    synchronized (locker) {
                        while (queue.isEmpty()) {
                            locker.wait();
                        }

                        long now = System.currentTimeMillis();
                        MyTimerTask head = queue.peek();
                        long waitTime = head.getTime() - now;

                        if (waitTime > 0) {
                            locker.wait(waitTime);
                            continue;
                        }

                        taskToRun = queue.poll(); // 先取出来
                    }

                    // 锁外执行,避免阻塞 schedule
                    taskToRun.run();
                }
            } catch (InterruptedException e) {
                // 允许线程退出(也可以选择恢复中断标记后退出)
                Thread.currentThread().interrupt();
            }
        });

        t.start();
    }

    public void schedule(Runnable task, long delay) {
        synchronized (locker) {
            queue.offer(new MyTimerTask(task, System.currentTimeMillis() + delay));
            locker.notify(); // 单线程工作者,notify 足够;更通用可 notifyAll
        }
    }
}

总结

  • 优先队列 保证“最早执行的任务”总在队首
  • 单工作线程 循环:队列空就等,时间没到就 wait(剩余时间),到点就执行
  • synchronized + wait/notify 做线程安全与条件同步

这就是一个计时器的骨架:简单、可推理、可扩展(后续还能加重复任务、取消任务、关闭计时器等能力)。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐