定时器的两种实现方式(优先级队列&时间轮)

定时器

定时器:
在某个时间到达之后, 执行指定的任务
这里介绍两个高效的定时器实现思路以及参考代码

基于优先级队列 / 堆

这种实现方式其实就是 Java 中的一种定时器类(Timer) 的模拟实现, 标准库中提供了一个 Timer 类, 它的核心方法是 schedule, schedule 包含了两个参数, 一个是即将要执行的任务的代码, 另一个参数是指定了多长时间之后要执行, 单位是毫秒(ms).

什么是优先级队列

队列是一种先进先出的数据结构, 但是在某些情况下, 需要存入的数据可能带有优先级, 就会有出队列时需要优先级高的元素先出队. 于是就衍生出了一种带有优先级的队列, 就是优先级队列(Priority Queue).
优先级队列的底层使用了堆的数据结构, 其中的元素都带有优先级(必须是可排序的), 其中按照排序的规则, 优先级高的元素排在堆顶, 每次插入或删除元素都会将优先级队列中的元素按照优先级高的在前的规则排列.

实现思路

假设当前有三个定时的任务需要执行, 分别是

任务执行时间
task112:00
task213:00
task314:00
  • 将这三个任务插入到优先级队列中, 此时定时器只要分配一个线程去检查队首元素, 查看是否到达执行时间即可. 如果队首元素还没到达待执行时间, 后续的元素一定没到达待执行时间, 因为在优先级队列中, 队首元素的优先级是最高的. 此时扫描线程就只需要盯住队首元素即可.
  • 另外, 扫描线程检查队首元素的时间间隔也不能太过频繁, 但是对于同时存在许多不同任务的定时器来说, 频繁的度是很难把控的, 并不能具体地设置等待时间来检查元素, 此时的做法就可以在检查完队首元素的时候, 根据队首元素要执行的时间来对比当前的时间, 让这个线程休眠到队首元素刚好要执行的时间即可. 此时系统再将这个线程唤醒, 使用这个方法, 就不需要高频地扫描队首元素, 能够节省 CPU 资源.
  • 在一个新的任务来了, 假如这个任务比队首元素执行时间要早的时候怎么办? 可以在添加新任务的时候, 唤醒一下刚才的线程, 重新扫描以下队首元素, 根据时间差调整阻塞时间即可.
实现代码解析
  1. Timer 类提供的核心接口是 schedule, 用于注册一个任务, 并指定该任务多长时间后执行.
public class Timer {
    public void schedule(Runnable command, long after) {
        // TODO
    }
}
  1. Task 类用于描述一个任务, 里面包含一个 Runnable 对象和一个 time(毫秒级时间戳). 因为这个对象要放到优先级队列中, 因此需要实现 Comparable 接口.
// 代表一个任务
class Task implements Comparable<Task> {
    private final Runnable runnable;
    private final long time;

    public Task(Runnable command, long time) {
        this.command = command;
        // time 中存的是绝对时间, 超过这个时间的任务就应该被执行
        this.time = System.currentTimeMillis() + time;
    }

    public void run() {
        command.run();
    }

    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(Task o) {
        return (int) (time - o.time);
    }
}
  1. Timer 的实例中, 通过 PriorityBlockingQueue 来组织若干个 Task 对象. 并通过 schedule 来往队列中插入一个个 Task 对象.
public class Timer {
    private final PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();

    // 将任务插入到队列中
    public void schedule(Runnable command, long after) {
        Task task = new Task(command, after);
        queue.offer(task);
    }
}
  1. Timer 类中存在一个 t 线程, 扫描队首元素并查看是否能够执行这个任务(表示时间已经到达).
public class Timer {
    private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();

    public Timer() {
        // 1. 取出元素, 查看时间
        // 时间还没到, 还不能执行该任务, 把该任务塞回去
        // 并休眠该线程直到时间到达
        // 时间到了, 执行任务
        Thread t = new Thread(() -> {
            while (true) {
                synchronized (this) {
                    try {
                        // 1. 取出元素, 查看时间
                        Task task = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (task.getTime() > curTime) {
                            // 时间还没到, 还不能执行该任务, 把该任务塞回去
                            // 并休眠该线程直到时间到达
                            queue.put(task);
                            this.wait(task.getTime() - curTime);
                        } else {
                            // 时间到了, 执行任务
                            task.run();
                        }
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        t.start();
    }
}
  1. 修改 Timer 中的 schedule 方法, 每次有新任务来的时候唤醒一下扫描线程(因为新任务可能需要立刻执行).
public void schedule(Runnable runnable, long time) {
    Task task = new Task(runnable, time);
    queue.put(task);

    synchronized (this) {
        this.notify();
    }
}
基于优先级队列的完整代码
public class Timer {
    private final PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();

    public Timer() {
        // 1. 取出元素, 查看时间
        // 时间还没到, 还不能执行该任务, 把该任务塞回去
        // 并休眠该线程直到时间到达
        // 时间到了, 执行任务
        Thread t = new Thread(() -> {
            while (true) {
                synchronized (this) {
                    try {
                        // 1. 取出元素, 查看时间
                        Task task = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (task.getTime() > curTime) {
                            // 时间还没到, 还不能执行该任务, 把该任务塞回去
                            // 并休眠该线程直到时间到达
                            queue.put(task);
                            this.wait(task.getTime() - curTime);
                        } else {
                            // 时间到了, 执行任务
                            task.run();
                        }
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        t.start();
    }

    // 将任务插入到队列中
    public void schedule(Runnable runnable, long time) {
        Task task = new Task(runnable, time);
        queue.put(task);

        synchronized (this) {
            this.notify();
        }
    }
}

// 代表一个任务
class Task implements Comparable<Task> {
    private final Runnable runnable;
    private final long time;

    public Task(Runnable runnable, long time) {
        this.runnable = runnable;
        // time 中存的是绝对时间, 超过这个时间的任务就应该被执行
        this.time = System.currentTimeMillis() + time;
    }

    public void run() {
        runnable.run();
    }

    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(Task o) {
        return (int) (time - o.time);
    }
}

基于时间轮

时间轮是一个存储定时任务的环形队列, 它的底层采用数组实现, 数组中的每个元素可以存放一个任务列表 (TimerTaskList), 任务列表是一个环形的双向链表, 这个链表的每一个元素都是一个定时任务项 (TimerTaskEntry), 其中封装了真正的定时任务 (TimerTask), 即真正要执行的任务.

一个基础的时间轮如下图所示.

时间轮的基本构成
  • tickMs (基本时间跨度): 时间轮由多个时间格组成, 每个时间格代表了当前时间轮的基本时间跨度.

    (可以想象一个时钟, 以秒为单位分成了 60 个刻度, 每个刻度都相当于时间轮当中的时间格, 秒针走一秒相当于走了一个时间跨度, 那么可以说这个时钟的基本时间跨度为 1 秒).

  • wheelSize (时间单位个数): 代表当前时间的时间格个数, 那么整个时间轮的总体时间跨度 (interval) 可以通过公式 tickMs * wheelSize 得到.

    (再次以时钟来举例, 将时钟上的一个刻度来表示一个时间单位, 即一个时间格. 那么总体时间跨度 (interval) 就是基本时间跨度 * 单位时间刻度 (tickMs * wheelSize ) = 1 * 60 = 60 秒).

  • currentTime (当前所处时间): 时间轮还有一个表盘指针 (currentTime), 用来表示时间轮的当前所处时间, currentTime 一定是 tickMs 的整数倍, currentTime 当前所指向的时间格代表里面的任务已经到达了执行时间了, 表示当前所指向的时间格的任务列表已经到期了, 需要立刻遍历当前的任务列表 (TimerTaskList) 并且执行里面的任务 (TimerTask).

时间轮的处理流程(第一种实现方法)

以上图为例, 当前时间轮的 tickMs = 1ms, wheelSize = 20, 计算出 interval = 20ms

  1. 初始情况下, 表盘指针 (currentTime) 指向时间格为 0 的位置, 如果此时定时器接受了一个定时为 5ms 的任务 (Task1) , 那么时间轮会计算当前任务应该放在时间格为 5 的任务列表 (TimerTaskList) 中.

  2. 随着时间推移, 指针 currentTime 不断推进, 当时间过去 5ms 之后, currentTime 就会指向时间格的第 5 格, 就需要将 TimerTaskList 中的任务做出相应的到期操作 (执行任务并删除任务重置当前任务列表为初始状态).

  3. 此时 currentTime 指向时间格为 5 的位置, 若此时定时器接受了一个定时为 10ms 的任务, 那么这个任务会存放在时间格为 15 的 TimerTaskList 中, 再过 10ms 指向第 15 格并执行当前 TimerTaskList 的过期处理.

此时 currentTime 指向第 15 个时间格, 如果此时有一个定时为 6ms 的任务插了进来, 怎么办?

  • 这时会将这个任务插入到第 1 个时间格(插入到原本已经到期的时间格 1 中, 其中的任务已经全部执行完并且删除, 任务列表也恢复到了初始的可以复用的状态).
流程中的问题(1)

这个时间轮的 interval 为 20ms, 如果此时有个定时为 350ms 的任务进来, 该如何处理?
是要直接扩充 wheelSize 的大小吗?

在许多业务场景中, 不乏几万甚至十几万毫秒的定时任务, 所以 wheelSize 的扩充没有底线, 而且如果定义一个拥有超大的 wheelSize 的时间轮, 会占用很大的内存空间并且执行效率也会变得低下. 这里引进一个解决方案: 层级时间轮

当当前要插入的任务的定时时间超过了当前时间轮的 interval, 就会尝试将这个任务添加到上层时间轮中.

可以看出, 第二层时间轮的单位时间跨度 (tickMs) 为第一层的总体时间跨度 (interval), 即二层时间轮的基本时间跨度 (tickMs) 为 20 ms, 总体时间跨度 (interval) 为 400ms, 也可以推算出第三层的 tickMs = 400ms, interval = 8000ms.

流程中的问题(2)
  1. 初始情况下, currentTime 指向第 0 格, 如果此时有一个定时为 350ms 的任务需要插进来, 此时以第一层来说先让不能满足条件, 所以会进行 时间轮的升级, 就有了第二层时间轮, 此时这个任务就会被插入到第二层的第 17 格对应的 TimerTaskList 中.

  2. 同理, 如果此时有一个定时为 450ms 的定时任务, 显然第二层时间轮也无法满足, 此时会再次触发时间轮的升级, 这个任务将会被放在第三层时间轮第 1 格的 TimerTaskList 中.

  3. 放在第三层第一格中的任务普遍都是到期时间在 [400ms, 800ms) 区间的任务, 但是时间格 1 所对应的超时时间为 400ms.

  4. 当第三层时间轮的 currentTime 指向第一格的时候(时间距离开始过了 400ms), 这一格所对应的 TimerTaskList 需要进行过期处理, 但是里面存放的 450ms 的任务并没有超时, 还剩下了 50ms 的时间, 不能执行这个任务的到期操作. 这里就要触发一个 时间轮的降级 操作, 会将这个剩余时间为 50ms 的任务重新提交到层级时间轮中, 因为第一层的 interval = 20 不满足条件, 所以会将这个任务放入第二层的到期时间为 [40ms, 60ms) 的时间格中. 即目前第二层时间轮的 currentTime 所指向的时间格的后两格所对应的任务列表中.

  5. 再次经历了 40ms, 这个任务所在的 TimerTaskList 也要进行过期操作, 不过这个任务还剩余 10ms 的过期时间, 此时又会触发一次时间轮的降级操作, 会将这个任务添加到第一层时间轮到期时间为 [10ms, 11ms) 的时间格中, 之后再经历 10ms 这个任务才真正到期, 执行相应的到期操作.

具体实现优化的思路

上述给出了这种实现方案的大体流程, 但是还有一些细节需要注意:

  • 时间轮的当前所处时间 (currentTime) 是当前系统时间 (timeMs) 的修剪, 但是只有第一层时间轮的起始时间 (startMs) 在最开始定时器被创建出来的时候被赋予了系统时间, 并且在最开始的时候 startMs 会进行修剪并被赋予到 currentTime 中, 因为每一层的 currentTime 必须是当前层的 tickMs 的整数倍, 以此与时间轮中的时间格的到期时间范围对应起来, 具体的修剪方法为: currentTime = startMs - (startMs % tickMs). 当有上层时间轮被创建出来的时候, 下层时间轮的 currentTime 会做为上层时间轮的 startMs 并根据公式修剪完赋值给上层的 currentTime.
  • currentTime 会随着时间的推移而推进, 唯一不变的是 currentTime 为 tickMs 的整数倍. 推进时会根据当前系统时间 (timeMs) 进行推进, 推进公式为 currentTime = timeMs - (timeMs % tickMs). 每次时间推进, 各个层级的时间轮的 currentTime 都会根据这个公式进行推进.
  • 定时器程序 (TimerLauncher) 只持有时间轮 (TimingWheel) 的第一层时间轮的引用, 并不会持有上层的时间轮, 但是每一层时间轮都会有它的上层时间轮引用 (overflowWheel), 以此层级调用可以实现定时器间接持有各个层级的时间轮的引用.
  • 这个程序借助了 JDK 中的 DelayQueue 来管理所有任务列表并协助推进时间轮. 将每个使用到的 TimerTaskList 都加入到 DelayQueue 中, DelayQueue 会根据每个 TimerTaskList 的超时时间 expiration 来排序, 越早过期的 TimerTaskList 会被排在 DelayQueue 的越靠近队头的地方, 即队头元素最早过期.

DelayQueue 如何协助推进时间轮?
当定时器程序启动的时候, 会使用一个单独的线程每隔一段时间让 DelayQueue 弹出已经过期的 TimerTaskList, 如果没有元素弹出, 就不做任何事情. 当有 TimerTaskList 被弹出的时候, 就会让所有层级的时间轮的指针 (currentTime) 推进, 并且对弹出的任务列表执行过期操作(执行里面的任务或者进行降级操作).
也就是说, 只有任务列表过期了, 时间轮的 currentTime 才会推进, 并不是每隔 tickMs 时间轮就会推进.

  • 如果没有 DelayQueue 来辅助推进, 假设定时器中最快过期的任务列表为 100ms, 采用每 1ms 定时推进的话, 将会有 99 次空推进 (即无效推进, 期间并没有任务要执行), 这样就会大程度地消耗系统资源. 使用 DelayQueue 来辅助推进可以做到 精准推进, 获取到超时的任务列表之后, 可以根据 TimerTaskList 中的过期时间 (expiration) 来推进时间轮的时间.
时间轮的处理流程(第二种实现方法)

这种实现方法基本上的流程都跟第一种实现方法大致相同. 但是在第一种方法中, 如果我目前的时间轮是最开始的时间轮, 它的 interval = 20ms, 在这时我插入一个 350ms 的定时任务, 就会触发 时间轮的升级, 会引出一个上层时间轮.

但是第二种方法中, 这里采取不同的方案, 不引入 层级时间轮 这个概念.

  1. 假如刚启动定时器, 此时 currentTime 指针指向第 0 格, 此时有一个 25ms 的定时任务进来了, 对于当前时间轮来说, 走一轮下来所消耗的时间为 20ms, 不能满足存放 25ms 的任务的条件, 此时就要把当前任务存放到第 5 格的位置, 相应算法为 定时时间 % interval = 25 % 20 = 5.

  2. 当时间过去 5ms 的时候, 这时 currentTime 指向第 5 格的位置, 这时需要做的操作就跟第一种实现方式不同了. 第二种实现方式中的 TimerTaskList 没有对应的过期时间, 里面的任务并不是走到这一格就执行, 而是 尝试 执行这个任务列表中的任务, 只有真正过期的任务才会被执行且在 TimerTaskList 中被删除. 此时时间过去了 5ms, 而刚才放入第 5 格的任务的定时时间为 25ms, 距离这个任务过期还有 20ms, 所以这个任务并不会被执行, 也不会从这个任务列表中被删除.

  3. 当时间再次过去 20ms, 这时 currentTime 相对于第二步来说又走了 20 格, 即走了一圈, 又回到了第 5 格, 这时再 尝试 执行这个 TimerTaskList 中的任务, 发现第一步添加进来的 25ms 的定时任务刚好过期, 这个任务才会被真正执行并且从这个任务列表中删除.

小结
第二种实现方式对比于第一种实现方式的优缺点:
优点:

  • 对比于第一种实现方式来说, 第二种实现方式节省了上层时间轮的内存开销, 整个定时器只使用了一个时间轮主体, 不用进行任务的时间轮升级以及降级操作, 也不用在多个时间轮中进行 currentTime 的同步等等…

缺点:

  • 因为只有一个时间轮主体, 对于某个业务来说, 这个时间轮的 tickMs, wheelSize 以及 interval 这三个基本构成需要对比于这个业务来调配适合的参数, 使得业务中添加进时间轮的任务能在这一个时间轮中分配均匀.
实现代码解析

这里相对于第一种实现方法来进行代码解析, 因为第一种实现方法更加的复杂, 只要能理解第一种方法的思路以及代码, 实现第二种时间轮绰绰有余.

  • 时间轮主体类

时间轮主体需要有上文提到过的属性来构成

  1. 基本时间跨度 (tickMs)
  2. 时间单位个数 (wheelSize)
  3. 总体时间跨度 (interval)
  4. 当前所处时间 (currentTime)
  5. 定时任务列表 (TimerTaskList)
  6. 上层时间轮 (overflowWheel)
  7. 辅助推进的延迟队列 (DelayQueue)

以及以下需要的方法

  1. 添加 TimerTaskEntry (add)
  2. 获取上层时间轮 (getOverflowWheel)
  3. 推进 currentTime (advanceCLock)

根据上面的流程推导, 可以在此类的构造方法中来初始化这个类, 其中用到了

  1. interval = tickMs * wheelSize;
  2. currentTime = currentTime - (currentTime % tickMs);

这两个公式来初始化时间轮的 interval 以及 currentTime

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.DelayQueue;

@Data
@Slf4j
public class TimeWheel {

    // 基本时间跨度
    private long tickMs;

    // 时间单位个数
    private int wheelSize;

    // 总体时间跨度
    private long interval;

    // 当前所处时间
    private long currentTime;

    // 定时任务列表
    private TimerTaskList[] buckets;

    // 上层时间轮
    private volatile TimeWheel overflowWheel;

    // 延迟队列, 协助推进时间轮
    private DelayQueue<TimerTaskList> delayQueue;

    public TimeWheel(long tickMs, int wheelSize, long currentTime, DelayQueue<TimerTaskList> delayQueue) {
        this.tickMs = tickMs;
        this.wheelSize = wheelSize;
        this.interval = tickMs * wheelSize;
        this.buckets = new TimerTaskList[wheelSize];
        this.currentTime = currentTime - (currentTime % tickMs);
        this.delayQueue = delayQueue;
        for (int i = 0; i < wheelSize; i++) {
            buckets[i] = new TimerTaskList();
        }
    }

    // 添加 TimerTaskEntry
    public boolean add(TimerTaskEntry entry) {
        // TODO
    }

    // 获取上层时间轮
    private TimeWheel getOverflowWheel() {
        // TODO
    }

    // 推进指针
    public void advanceCLock(long timestamp) {
        // TODO
    }
}

获取上层时间轮需要考虑两种情况

  1. 上层时间轮已经被创建了
  2. 上层时间轮还未被创建

如果已经被创建了, 那么直接返回 overflowWheel 就行, 但是未被创建就需要创建一个上层时间轮, 但是每一个时间轮的上一层时间轮有且只有一个, 考虑到定时器是一个多线程的程序, 那么创建就要考虑到线程安全问题.
所以这里可以使用单例模式的思想来创建当前时间轮的上层时间轮

// 获取上层时间轮
private TimeWheel getOverflowWheel() {
    if (overflowWheel == null) {
        synchronized (this) {
            if (overflowWheel == null) {
                overflowWheel =
                    new TimeWheel(interval, wheelSize, currentTime, delayQueue);
            }
        }
    }
    return overflowWheel;
}

推进指针会根据传过来的 timestamp 元素来推进. timestamp 是 DelayQueue 在弹出一个刚好过期的元素的时候传过来的当前元素的过期时间, 这样就可以做到 “只有当一个任务列表过期之后, 时间轮才会被推进” 这个前提. 根据上文流程所给出的 修剪 currentTime 的公式: currentTime = timeMs - (timeMs % tickMs) 来推进时间轮.

// 推进指针
public void advanceCLock(long timestamp) {
    if (timestamp > currentTime + tickMs) {
        currentTime = timestamp - (timestamp % tickMs);
        if (overflowWheel != null) {
            this.getOverflowWheel().advanceCLock(timestamp);
        }
    }
}

添加任务到任务列表当中首先要先判断当前任务有没有过期, 如没过期再继续判断这个任务适合哪一层级的时间轮并且放进去

// 添加 TimerTaskEntry
public boolean add(TimerTaskEntry entry) {
    long expiration = entry.getExpireMs();
    if (expiration < tickMs + currentTime) {
        // 定时任务到期
        return false;
    } else if (expiration < currentTime + interval) {
        // 扔进当前时间轮的某个槽里, 只有时间大于某个槽, 才会放进去
        long virtualId = (expiration / tickMs);
        int index = (int) (virtualId % wheelSize);
        TimerTaskList bucket = buckets[index];
        bucket.addTask(entry);
        // 设置 bucket 过期时间
        if (bucket.setExpiration(virtualId * tickMs)) {
            // 设好过期时间的 bucket 需要入队
            delayQueue.offer(bucket);
            return true;
        }
    } else {
        // 当前轮不能满足, 需要扔到上一轮
        TimeWheel timeWheel = getOverflowWheel();
        return timeWheel.add(entry);
    }
    return false;
}
  • TimerTaskEntry 主体

TimerTaskEntry 是 TimerTaskList 中的一个节点, TimerTaskList 是一个双向环形链表, 所以 TimerTaskEntry 个体要实现链表的操作, 就要有如下两个字段

  1. TimerTaskEntry next;
  2. TimerTaskEntry prev;

这两个字段表示一个双向链表中的下一个和前一个节点. next 字段指向链表中的下一个节点, 而 prev 字段指向链表中的前一个节点. 这些字段用于将定时任务条目连接到定时任务列表中, 从而实现链表的操作, 比如添加和移除定时任务.

想要知道这个任务所在的任务列表, 就需要如下的字段
3. volatile TimerTaskList timerTaskList;

这个字段表示定时任务所属的定时任务列表 (TimerTaskList). 它使用 volatile 关键字修饰, 表明在多线程环境中, 该字段的值可能会被不同线程修改, 需要保证可见性和线程安全性.通过这个字段, 可以知道定时任务当前所在的任务列表.

想要知道这个任务具体定时任务对象, 就需要如下的字段

  1. private TimerTask timerTask;

这个字段表示具体的定时任务对象, 它是实际需要执行的任务. 在定时任务条目中, 这个字段用来存储任务的引用, 以便在任务到期时执行相应的操作.

以及最后一个字段: 过期时间

  1. private long expireMs;

这个字段表示定时任务的过期时间, 以毫秒为单位.它表示定时任务应该在什么时间点执行. 当系统时间达到或超过 expireMs 的值时, 就会触发执行相应的定时任务.

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

@Data
@Slf4j
public class TimerTaskEntry implements Comparable<TimerTaskEntry> {

    volatile TimerTaskList timerTaskList;
    TimerTaskEntry next;
    TimerTaskEntry prev;
    private TimerTask timerTask;
    private long expireMs;

    public TimerTaskEntry(TimerTask timerTask, long expireMs) {
        this.timerTask = timerTask;
        this.expireMs = expireMs;
        this.next = null;
        this.prev = null;
    }

    // 删除 TimerTaskList 中对应的 TimerTaskEntry 节点
    public void remove() {
        TimerTaskList currentList = timerTaskList;
        while (currentList != null) {
            currentList.remove(this);
            currentList = timerTaskList;
        }
    }

    @Override
    public int compareTo(TimerTaskEntry o) {
        return (int) (this.expireMs - o.expireMs);
    }
}
  • TimerTaskList 主体

TimerTaskList 是要存放到 DelayQueue 中的, 所以要实现 Delayed 接口并重写 getDelay 方法, 返回当前任务列表的过期时间

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;

@Data
@Slf4j
public class TimerTaskList implements Delayed {
    // TimerTaskList 环形链表使用一个虚拟根节点 root
    private TimerTaskEntry root = new TimerTaskEntry(null, -1);

    // bucket 的过期时间
    private AtomicLong expiration = new AtomicLong(-1L);

    {
        root.next = root;
        root.prev = root;
    }

    public long getExpiration() {
        return expiration.get();
    }

    // 设置bucket的过期时间, 设置成功返回 true
    boolean setExpiration(long expirationMs) {
        return expiration.getAndSet(expirationMs) != expirationMs;
    }

    public void addTask(TimerTaskEntry entry) {
        boolean done = false;
        while (!done) {
            // 如果 TimerTaskEntry 已经在别的 list 中, 就先在代码块外面移除, 避免死锁, 一直到成功为止
            entry.remove();
            synchronized (this) {
                if (entry.timerTaskList == null) {
                    // 加到链表的末尾
                    entry.timerTaskList = this;
                    TimerTaskEntry tail = root.prev;
                    entry.prev = tail;
                    entry.next = root;
                    tail.next = entry;
                    root.prev = entry;
                    done = true;
                }
            }
        }
    }

    // 从 TimedTaskList 移除指定的 timerTaskEntry
    public void remove(TimerTaskEntry entry) {
        synchronized (this) {
            if (entry.getTimerTaskList().equals(this)) {
                entry.next.prev = entry.prev;
                entry.prev.next = entry.next;
                entry.next = null;
                entry.prev = null;
                entry.timerTaskList = null;
            }
        }
    }

    // 移除所有节点
    public synchronized void clear(Consumer<TimerTaskEntry> entry) {
        // TODO
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return Math.max(0, unit.convert(expiration.get() - System.currentTimeMillis(), TimeUnit.MILLISECONDS));
    }

    @Override
    public int compareTo(Delayed o) {
        if (o instanceof TimerTaskList) {
            return Long.compare(expiration.get(), ((TimerTaskList) o).expiration.get());
        }
        return 0;
    }
}

其中, clear 方法的作用是移除所有节点 (任务项) 并执行一个消费者函数 entry.accept(head) 来处理被移除的节点.

  1. 方法签名: public synchronized void clear(Consumer entry), 接受一个类型为 Consumer 的参数, 用于处理被移除的节点.

  2. 获取链表头节点: 通过 TimerTaskEntry head = root.next; 获取链表的头节点, 初始时 root 节点是虚拟根节点, 其 next 引用指向第一个实际节点.

  3. 循环遍历链表: 使用 while 循环遍历链表, 直到链表为空, 即头节点 head 回到了虚拟根节点 root. 循环的目的是逐个移除链表中的节点并执行消费者函数.

  4. 移除节点: 在每次迭代中, 调用 remove(head) 方法来从链表中移除头节点 head. 这个操作会将 head 从链表中断开, 并将链表的 prev 和 next 引用重新连接起来, 同时将 head 的 timedTaskList 引用置为 null, 表示节点已经不再属于任何任务列表.

  5. 处理被移除的节点: 调用 entry.accept(head), 执行消费者函数 entry 并传入被移除的节点 head. 这个函数可以用于处理被移除的节点, 例如执行节点的任务或者进行其他操作, 在后面主程序类中有具体的操作.

  6. 更新过期时间: 在链表清空后, 通过 expiration.set(-1L); 将链表的过期时间设置为 -1L, 表示链表不再包含任何任务项.

// 移除所有节点
public synchronized void clear(Consumer<TimerTaskEntry> entry) {
    TimerTaskEntry head = root.next;
    while (!head.equals(root)) {
        remove(head);
        entry.accept(head);
        head = root.next;
    }
    expiration.set(-1L);
}
  • TimerTask 主体
    这个类实现了 Runnable 接口, 它是任务的实体, 定时器过期之后所要执行的任务写在 run 方法中
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

@Data
@Slf4j
public class TimerTask implements Runnable {

    // 延时时间
    private long delayMs;

    // 任务所在的 entry
    private TimerTaskEntry timerTaskEntry;

    // 任务描述信息
    private String desc;

    public TimerTask(String desc, long delayMs) {
        this.desc = desc;
        this.delayMs = delayMs;
        this.timerTaskEntry = null;
    }

    public synchronized void setTimerTaskEntry(TimerTaskEntry entry) {
        // 如果这个 TimerTask 已经被一个已存在的 TimerTaskEntry 持有, 先移除一个
        if (timerTaskEntry != null && timerTaskEntry != entry) {
            timerTaskEntry.remove();
        }
        timerTaskEntry = entry;
    }

    @Override
    public void run() {
        log.info("============={}任务执行", desc);
    }
}
  • TimerLauncher 主体

这个是整个定时器的实现类

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.*;

@Data
@Slf4j
public class TimerLauncher {

    // 底层时间轮
    private TimeWheel timeWheel;

    // 一个 Timer 只有一个延时队列
    private DelayQueue<TimerTaskList> delayQueue = new DelayQueue<>();

    // 过期任务执行线程
    private ExecutorService workerThreadPool;

    // 轮询 DelayQueue 获取过期任务线程
    private ScheduledExecutorService bossThreadPool;


    public TimerLauncher() {
        this.timeWheel = new TimeWheel(1, 20, System.currentTimeMillis(), delayQueue);
        this.workerThreadPool = Executors.newFixedThreadPool(100);
        this.bossThreadPool = Executors.newScheduledThreadPool(1); // 使用 ScheduledExecutorService
    }


    private void addTimerTaskEntry(TimerTaskEntry entry) {
        if (!timeWheel.add(entry)) {
            // 任务已到期
            TimerTask timerTask = entry.getTimerTask();
            log.info("=====任务:{} 已到期,准备执行============", timerTask.getDesc());
            workerThreadPool.submit(timerTask);
        }
    }

    public void add(TimerTask timerTask) {
        log.info("=======添加任务开始====task:{}", timerTask.getDesc());
        TimerTaskEntry entry = new TimerTaskEntry(timerTask, timerTask.getDelayMs() + System.currentTimeMillis());
        timerTask.setTimerTaskEntry(entry);
        addTimerTaskEntry(entry);
    }

    /**
     * 推动指针运转获取过期任务
     *
     * @param timeout 时间间隔
     */
    private synchronized void advanceClock(long timeout) {
        // TODO
    }

    // 启动定时器
    public void start() {
        // 每 20ms 推动一次时间轮运转
        this.bossThreadPool.scheduleAtFixedRate(() -> {
            this.advanceClock(20);
        }, 0, 20, TimeUnit.MILLISECONDS);
    }

    // 关闭定时器
    public void shutdown() {
        this.bossThreadPool.shutdown();
        this.workerThreadPool.shutdown();
        this.timeWheel = null;
    }
}

advanceClock 是时间轮的一个核心方法, 用于推进时间轮的时钟并处理到期的定时任务. 在主程序启动的时候, 会有一个线程每隔一段时间调用这个方法, 而这个方法需要实现的操作是:

  1. 在方法内部首先调用 delayQueue 的 poll 方法, 该方法会尝试从队列中获取到期时间最近的 TimerTaskList 对象. timeout 参数表示最长等待时间, 如果在指定时间内没有到期的任务, 会返回 null.
  2. 如果成功获取到一个到期的 TimerTaskList 对象, 说明有定时任务已经到期.
  3. 调用时间轮的 advanceClock 方法, 将时间轮的当前时间推进到到期的 TimerTaskList 的过期时间. 这样可以确保时间轮的时钟与实际时间保持同步, 同时也触发了定时任务的执行.
  4. 调用 TimerTaskList 的 clear 方法, 清空到期的任务列表, 并使用传入的消费者函数 this::addTimerTaskEntry 处理被移除的任务. 这个步骤实际上是执行到期的定时任务, 包括执行任务的降级操作.
private synchronized void advanceClock(long timeout) {
    try {
        TimerTaskList bucket = delayQueue.poll(timeout, TimeUnit.MILLISECONDS);
        if (bucket != null) {
            // 推进时间
            timeWheel.advanceCLock(bucket.getExpiration());
            // 执行过期任务(包含降级)
            bucket.clear(this::addTimerTaskEntry);
        }
    } catch (InterruptedException e) {
        log.error("advanceClock error");
    }
}
基于时间轮的完整代码
  • 定时器实现部分
  1. 定时器实现 (TimerLauncher)
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.*;

@Data
@Slf4j
public class TimerLauncher {

    // 底层时间轮
    private TimeWheel timeWheel;

    // 一个 Timer 只有一个延时队列
    private DelayQueue<TimerTaskList> delayQueue = new DelayQueue<>();

    // 过期任务执行线程
    private ExecutorService workerThreadPool;

    // 轮询 delayQueue 获取过期任务线程
    private ScheduledExecutorService bossThreadPool;


    public TimerLauncher() {
        this.timeWheel = new TimeWheel(1, 20, System.currentTimeMillis(), delayQueue);
        this.workerThreadPool = Executors.newFixedThreadPool(100);
        this.bossThreadPool = Executors.newScheduledThreadPool(1); // 使用 ScheduledExecutorService
    }


    private void addTimerTaskEntry(TimerTaskEntry entry) {
        if (!timeWheel.add(entry)) {
            // 任务已到期
            TimerTask timerTask = entry.getTimerTask();
            log.info("=====任务:{} 已到期,准备执行============", timerTask.getDesc());
            workerThreadPool.submit(timerTask);
        }
    }

    public void add(TimerTask timerTask) {
        log.info("=======添加任务开始====task:{}", timerTask.getDesc());
        TimerTaskEntry entry = new TimerTaskEntry(timerTask, timerTask.getDelayMs() + System.currentTimeMillis());
        timerTask.setTimerTaskEntry(entry);
        addTimerTaskEntry(entry);
    }

    /**
     * 推动指针运转获取过期任务
     *
     * @param timeout 时间间隔
     */
    private synchronized void advanceClock(long timeout) {
        try {
            TimerTaskList bucket = delayQueue.poll(timeout, TimeUnit.MILLISECONDS);
            if (bucket != null) {
                // 推进时间
                timeWheel.advanceCLock(bucket.getExpiration());
                // 执行过期任务(包含降级)
                bucket.clear(this::addTimerTaskEntry);
            }
        } catch (InterruptedException e) {
            log.error("advanceClock error");
        }
    }

    // 启动定时器
    public void start() {
        // 每 20ms 推动一次时间轮运转
        this.bossThreadPool.scheduleAtFixedRate(() -> {
            this.advanceClock(20);
        }, 0, 20, TimeUnit.MILLISECONDS);
    }

    // 关闭定时器
    public void shutdown() {
        this.bossThreadPool.shutdown();
        this.workerThreadPool.shutdown();
        this.timeWheel = null;
    }
}
  • 时间轮主体部分
  1. 时间轮 (TimingWheel)
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.DelayQueue;

@Data
@Slf4j
public class TimeWheel {

    // 基本时间跨度
    private long tickMs;

    // 时间单位个数
    private int wheelSize;

    // 总体时间跨度
    private long interval;

    // 当前所处时间
    private long currentTime;

    // 定时任务列表
    private TimerTaskList[] buckets;

    // 上层时间轮
    private volatile TimeWheel overflowWheel;

    // 延迟队列, 协助推进时间轮
    private DelayQueue<TimerTaskList> delayQueue;


    public TimeWheel(long tickMs, int wheelSize, long currentTime, DelayQueue<TimerTaskList> delayQueue) {
        this.tickMs = tickMs;
        this.wheelSize = wheelSize;
        this.interval = tickMs * wheelSize;
        this.buckets = new TimerTaskList[wheelSize];
        this.currentTime = currentTime - (currentTime % tickMs);
        this.delayQueue = delayQueue;
        for (int i = 0; i < wheelSize; i++) {
            buckets[i] = new TimerTaskList();
        }
    }

    // 添加 TimerTaskEntry
    public boolean add(TimerTaskEntry entry) {
        long expiration = entry.getExpireMs();
        if (expiration < tickMs + currentTime) {
            // 定时任务到期
            return false;
        } else if (expiration < currentTime + interval) {
            // 扔进当前时间轮的某个槽里, 只有时间大于某个槽, 才会放进去
            long virtualId = (expiration / tickMs);
            int index = (int) (virtualId % wheelSize);
            TimerTaskList bucket = buckets[index];
            bucket.addTask(entry);
            // 设置 bucket 过期时间
            if (bucket.setExpiration(virtualId * tickMs)) {
                // 设好过期时间的 bucket 需要入队
                delayQueue.offer(bucket);
                return true;
            }
        } else {
            // 当前轮不能满足, 需要扔到上一轮
            TimeWheel timeWheel = getOverflowWheel();
            return timeWheel.add(entry);
        }
        return false;
    }

    // 获取上层时间轮
    private TimeWheel getOverflowWheel() {
        if (overflowWheel == null) {
            synchronized (this) {
                if (overflowWheel == null) {
                    overflowWheel =
                            new TimeWheel(interval, wheelSize, currentTime, delayQueue);
                }
            }
        }
        return overflowWheel;
    }

    // 推进指针
    public void advanceCLock(long timestamp) {
        if (timestamp > currentTime + tickMs) {
            currentTime = timestamp - (timestamp % tickMs);
            if (overflowWheel != null) {
                this.getOverflowWheel().advanceCLock(timestamp);
            }
        }
    }
}
  • 定时任务部分
  1. 定时任务项 (TimerTaskEntry)
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

@Data
@Slf4j
public class TimerTaskEntry implements Comparable<TimerTaskEntry> {

    volatile TimerTaskList timerTaskList;
    TimerTaskEntry next;
    TimerTaskEntry prev;
    private TimerTask timerTask;
    private long expireMs;

    public TimerTaskEntry(TimerTask timerTask, long expireMs) {
        this.timerTask = timerTask;
        this.expireMs = expireMs;
        this.next = null;
        this.prev = null;
    }

    // 删除 TimerTaskList 中对应的 TimerTaskEntry 节点
    public void remove() {
        TimerTaskList currentList = timerTaskList;
        while (currentList != null) {
            currentList.remove(this);
            currentList = timerTaskList;
        }
    }

    @Override
    public int compareTo(TimerTaskEntry o) {
        return (int) (this.expireMs - o.expireMs);
    }
}
  1. 定时任务列表 (TimerTaskList)
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;

@Data
@Slf4j
public class TimerTaskList implements Delayed {

    // TimerTaskList 环形链表使用一个虚拟根节点 root
    private TimerTaskEntry root = new TimerTaskEntry(null, -1);

    // bucket 的过期时间
    private AtomicLong expiration = new AtomicLong(-1L);

    {
        root.next = root;
        root.prev = root;
    }

    public long getExpiration() {
        return expiration.get();
    }

    // 设置bucket的过期时间, 设置成功返回 true
    boolean setExpiration(long expirationMs) {
        return expiration.getAndSet(expirationMs) != expirationMs;
    }

    public void addTask(TimerTaskEntry entry) {
        boolean done = false;
        while (!done) {
            // 如果 TimerTaskEntry 已经在别的 list 中, 就先在代码块外面移除, 避免死锁, 一直到成功为止
            entry.remove();
            synchronized (this) {
                if (entry.timerTaskList == null) {
                    // 加到链表的末尾
                    entry.timerTaskList = this;
                    TimerTaskEntry tail = root.prev;
                    entry.prev = tail;
                    entry.next = root;
                    tail.next = entry;
                    root.prev = entry;
                    done = true;
                }
            }
        }
    }

    // 从 TimedTaskList 移除指定的 timerTaskEntry
    public void remove(TimerTaskEntry entry) {
        synchronized (this) {
            if (entry.getTimerTaskList().equals(this)) {
                entry.next.prev = entry.prev;
                entry.prev.next = entry.next;
                entry.next = null;
                entry.prev = null;
                entry.timerTaskList = null;
            }
        }
    }

    // 移除所有节点
    public synchronized void clear(Consumer<TimerTaskEntry> entry) {
        TimerTaskEntry head = root.next;
        while (!head.equals(root)) {
            remove(head);
            entry.accept(head);
            head = root.next;
        }
        expiration.set(-1L);
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return Math.max(0, unit.convert(expiration.get() - System.currentTimeMillis(), TimeUnit.MILLISECONDS));
    }

    @Override
    public int compareTo(Delayed o) {
        if (o instanceof TimerTaskList) {
            return Long.compare(expiration.get(), ((TimerTaskList) o).expiration.get());
        }
        return 0;
    }
}
  1. 真正的定时任务 (TimerTask)
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

@Data
@Slf4j
public class TimerTask implements Runnable {

    // 延时时间
    private long delayMs;

    // 任务所在的 entry
    private TimerTaskEntry timerTaskEntry;

    // 任务描述信息
    private String desc;

    public TimerTask(String desc, long delayMs) {
        this.desc = desc;
        this.delayMs = delayMs;
        this.timerTaskEntry = null;
    }

    public synchronized void setTimerTaskEntry(TimerTaskEntry entry) {
        // 如果这个 TimerTask 已经被一个已存在的 TimerTaskEntry 持有, 先移除一个
        if (timerTaskEntry != null && timerTaskEntry != entry) {
            timerTaskEntry.remove();
        }
        timerTaskEntry = entry;
    }

    @Override
    public void run() {
        log.info("============={}任务执行", desc);
    }
}
基于时间轮代码使用方法
  1. 首先, 创建一个定时任务类 MyTimerTask, 继承自 TimerTask 并实现 run 方法, 定义具体的定时任务操作:
@Slf4j
class MyTimerTask extends TimerTask {

    private String taskName;

    public MyTimerTask(String taskName, long delayMs) {
        super(taskName, delayMs);
        this.taskName = taskName;
    }

    @Override
    public void run() {
        log.info("执行当前任务: {}", taskName);
        // DO SOMETHING ...
    }
}
  1. 接下来, 创建一个 TimerLauncher 实例并添加定时任务:
public class Example {
    public static void main(String[] args) throws InterruptedException {
        TimerLauncher timerLauncher = new TimerLauncher();

        // 添加定时任务
        timerLauncher.add(new MyTimerTask("Task1", 5000)); // 5秒后执行
        timerLauncher.add(new MyTimerTask("Task2", 10000)); // 10秒后执行
        timerLauncher.start();

        // 在这里可以进行其他操作

        // 等待一段时间, 确保定时任务有足够的时间执行
        Thread.sleep(15000);

        // 关闭定时器
        timerLauncher.shutdown();
    }
}

接下来启动程序, 可以看到运行日志

可以看到两个任务分别在启动之后的 5s 和 10s 后定期执行了任务

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值