基于时间轮的定时任务组件

18 阅读3分钟

时间轮-【gitee】

使用了一些jdk9+的特性语法,但是可以降级到jdk8甚至更低,功能没变化只是缺少了一些jdk高版本的优化。

可以自定义时间推进粒度()和时间轮格子大小。 支持定时执行,循环执行,以及随机下次执行时间。

主要分为两个组件,DripTimeWheel.javaDripTicker.java

一个是时间轮本体,一个是时间推进器。

关于时间推进器

  • 独立高优先级线程,以固定周期(如 10ms)精准推进“tick”。
  • 自然秒对齐为起点,启动时自动校准,初始误差通常小于 1 毫秒。
        // 以一个自然秒的开始作为基准时间开始推进,误差通常在1ms内
        // 获取当前时间距离上个整秒的差值纳秒
        // 估算整秒起点对应的纳秒时间(这两个时间无法原子获取,但误差 <1ms)
        long currentTimeMillis = System.currentTimeMillis();
        long nowNanos = System.nanoTime();

        long nanos = TimeUnit.MILLISECONDS.toNanos(currentTimeMillis % 1000);
        final long startNanos = nowNanos - nanos;
  • 采用 混合等待策略:长时间等待使用 LockSupport.parkNanos 释放 CPU,短时间(<2ms)使用 Thread.onSpinWait() 自旋,兼顾精度与功耗。

   // ------------------------------ 混合等待策略 ------------------------------
    private long awaitUntilTargetTime(long targetNanos) {
        while (true) {
            long currentNanos = System.nanoTime();
            long remainingNanos = targetNanos - currentNanos;
            // 1. 已到达/超出目标时间,返回剩余时间(负数/零)
            if (remainingNanos <= 0) {
                return remainingNanos;
            }

            // 2. 剩余时间>10ms:阻塞99%的时间(释放CPU,低消耗),阻塞一次后一定能进入自旋,通常剩余1ms以下
            if (remainingNanos > SPIN_THRESHOLD_NANOS) {
                LockSupport.parkNanos(remainingNanos - 100_000); // 留0.2ms自旋
            } else {
                // 3. 剩余时间≤10ms:智能自旋(无唤醒延迟,高精度)
                // 这个是jdk9的优化,不优化也行,就是资源消耗多一点点
                Thread.onSpinWait();
            }

            // 4. 响应关闭/中断信号
            if (!isRunning || isShutdown || Thread.currentThread().isInterrupted()) {
                return 0;
            }
        }
    }
  • 支持自动追赶:若因 GC 或系统卡顿跳过多个 tick,会根据当前系统时间直接跳转到正确的 tick,避免任务堆积或漏调。
  • 有一个专门的单线程负责推进时间不会被其他任务占用

关于时间轮

  • 数据结构分为主轮和溢出区
    • 主轮存放马上要执行的任务,负责高效存取执行任务。
    • 溢出区存放等待时间长,暂时不需要执行的任务,会在合适的时间取出放入主轮
  • 预加载机制:每次推进时,将未来一轮内即将到期的任务从溢出区加载到主轮,确保主轮始终“热”。
  • 专门的单线程池负责移动存取任务,保证整个时间轮结构不会有并发问题。
  • 用户自己传入真正执行任务的线程池,根据同一时刻的任务峰值调整任务线程池大小,否则会因此卡住

关于task

会有专门的Task结构保存任务,可以设置延迟执行时间、循环执行、取消等操作。 取消操作可以随时执行,但是取消的任务无法再次发起

示例

    /**
     * 使用100ms的时间推进,这里用256格的时间轮,25.6秒一轮
     **/
    private static final DripTimeWheel dripTimeWheel = new DripTimeWheel(
            100,
            256,
            (_, task) -> task.getTask().run(),
            5000,
            executorService
    );
    
    /**
     * 固定延迟执行定时任务,并以固定时间循环
     **/
    public DripTask scheduleAtFixedRate(Runnable command, Duration delay, Duration period) {
        DripFixedCycleTask dripTask = new DripFixedCycleTask(dripTimeWheel.getTicker(), command, delay, period);
        dripTimeWheel.schedule(dripTask);
        return dripTask;
    }

    /**
     * 固定延迟执行定时任务,以随机范围内的时间循环
     **/
    public DripTask scheduleAtRandomRate(Runnable command, Duration delay, Duration minPeriod, Duration maxPeriod) {
        DripRandomCycleTask dripTask = new DripRandomCycleTask(dripTimeWheel.getTicker(), command, delay, minPeriod, maxPeriod);
        dripTimeWheel.schedule(dripTask);
        return dripTask;
    }

    /**
     * 固定延迟执行
     **/
    public DripTask scheduleAt(Runnable command, Duration delay) {
        DripDelayTask dripTask = new DripDelayTask(dripTimeWheel.getTicker(), command, delay);
        dripTimeWheel.schedule(dripTask);
        return dripTask;
    }