时间轮的设计原理及应用方式详解,实现单线程调度多个延时任务

877 阅读18分钟

1 前言

相信定时器大家都很熟悉,当有一个需要延迟执行的任务,或者说需要定时轮询的任务时,我只需要构建一个定时器,将任务通过Runnable进行一次封装,设置一个时间,然后直接往里丢就完事了,就像这样。

  • 通过自定义延迟线程池,添加一个延迟执行的任务:
    // 定义一个延迟线程池
    private ScheduledThreadPoolExecutor scheduledTimer = new ScheduledThreadPoolExecutor(1);
    
    /**
     * 添加一个延迟执行的任务
     * @param r 任务本身
     * @param delayedSeconds 延迟多少秒执行
     */
    public void addTaskScheduledTimer(Runnable r, long delayedSeconds) {
        scheduledTimer.schedule(r, delayedSeconds, TimeUnit.SECONDS);
    }
    
  • 通过自定义延迟线程池,添加一个周期性执行的定时任务
    // 定义一个延迟线程池
    private ScheduledThreadPoolExecutor scheduledTimer = new ScheduledThreadPoolExecutor(1);
    
    /**
     * 添加一个定时任务,固定频率执行
     * @param r 任务本身
     * @param rateSeconds 固定多少秒执行
     */
    public void scheduleTaskWithFixedRate(Runnable r, long rateSeconds) {
        scheduledTimer.scheduleAtFixedRate(r, 0, rateSeconds, TimeUnit.SECONDS);
    }
    
    /**
     * 添加一个定时任务,固定延迟执行
     * @param r 任务本身
     * @param delayedSeconds 每次任务执行完后固定等待多少秒再执行下一次
     */
    public void scheduleTaskWithFixedDelay(Runnable r, long delayedSeconds) {
        scheduledTimer.scheduleWithFixedDelay(r, 0, delayedSeconds, TimeUnit.SECONDS);
    }
    

这没有问题,大部分情况下大家也都是这样实现的,只不过大家的实现方式可能更加优雅,比如:有用spring注解的:

/**
 * 每天中午12点执行
 */
@Scheduled(cron = "0 0 12 * * ?")
public void addTask() {
    // do your business
}

还有的同学考虑到微服务分布式集群下,多点重复定时任务的并发控制,选择使用第三方中间件比如xxl-job,亦或是自己实现分布式定时器框架,都无可厚非,但本质都是换汤不换药。

前些天我的朋友公司招人,作为面试官的他说在面试的时候问到了一个关于业务系统中不同任务采用不同定时器的问题,设定的需求场景是:

  1. 当前业务系统A与业务系统B存在数据下发业务场景。
  2. A可基于页面配置数据下发时间点,比如:我可以在今天设置明天或者下周的时候下发怎么样的数据。时间到后自动下发给B。
  3. A系统最大规格可同时配置2w条记录。

不出意外地,被面试的同学都回答,可以使用定时线程池,但被问到线程池相关原理,并问到是否2w条记录每一条都需要配置一个线程时,我朋友表示很无奈,跟我吐槽说,难道他们都不考虑线程池大小和cpu调度性能吗?我很理解,如果开发经验不够丰富,或者没有研究过相关底层的工具的同学,很难成立这样的思维,那使用普通的线程池进行定时调度会有什么问题呢?

2 使用普通线程池调度大量不同时间点触发任务的缺点

试想一下,如果2w个任务,配置的均是不同的时间点触发,而根据前文中的代码样例进行配置,则需要给每个任务分配一个线程池,或者干脆间隔一小段时间查询数据库并遍历所有数据,满足条件的则立刻触发,或者构建专属的ScheduledThreadPoolExecutor线程池并添加2w个任务。

2.1 给每个任务分配一个线程池的缺陷

自然是浪费内存和线程资源,所需要的内存和cpu资源随数据的增加而线性增加,这是不可控的,很容易随业务量的增加而将业务服务干崩。

2.2 数据库轮询遍历的缺陷

  1. 每次查询开销较大,性能欠佳
  2. 返回的数据量随业务量增大而增大,对内存不友好,采用分页分批查询,调度性能又很差
  3. 很容易出现“执行时间不精确”的问题

2.3 使用单个线程池添加任务

可以解决“给每个任务分配一个线程池的缺陷”,解决cpu的占用问题,内存使用也能得到缓解,似乎能够解决问题,但该线程池仍不够精简,存在一定的资源浪费,并且太灵活的设置“线程池七大参数”的地方,会让使用者一个不小心,设置参数不合理,会造成过多的线程处于阻塞状态,不够优雅。

3 能不能使用单线程来调度2w个不同时机触发的任务呢

答案自然是能!

这显然是比较优雅的方案,比较适合前言中提到的业务场景,一般来说,我并不会时时刻刻达到满规格状态,可能平时就只会配置10来个20来个下发任务,并且大多情况都是不同时间点触发,那我完全可以只阻塞一个线程,然后通过一个线性表(通常是数组)来维护等待中的任务,就行了!

这样既能够使内存资源占用最小化,也能够使cpu资源利用率最大化。有的同学可能要问了,那万一我配置了多个同一时间点触发的任务怎么办呢,如果使用单线程调度,在执行一个任务时,其它需要同时执行的任务此时只能进行等待,若一个任务执行下发需要30ms,后面需要理论上同时执行的任务,实际上真实执行则会存在延迟,如何解决呢?

  1. 业务上进行评估,在业务层面功能规格进行限制,单个任务延迟最大时间进行说明,并以此时间为准限制统一时间点配置的最大任务数量

    没错,这并不是一个技术解决方案,在很多时候,业务层面能够解决的,技术上就可以不解决。很简单粗暴对不对,但这往往是最有效的,能在很大程度上解决资源不足的问题,还能大大降低系统复杂度

  2. 设置临时线程数,同样需要业务侧限制同一时间点设置的最多并发数,为了精准控制所有任务的执行时间,在该时间点获取到同样的数据时,根据数据量初始化临时的线程进行执行,执行完毕后释放

    有时候技术和业务是互相权衡互相弥补的

说了半天,还没说到正事,那就是如何来实现呢?

其实有一个概念叫“时间轮”,这个东西在很多地方都用到,比如:netty、dubbo等,并且不同的框架实现的方式都不一样,但其核心思想都是一样的,在本文中以netty中的时间轮框架来进行说明,先上一段使用代码:

  • 添加一个延时执行的任务
    // 定义一个时间轮
    private HashedWheelTimer wheelTimer = new HashedWheelTimer();
    
    public void addTaskWithWheelTimer(Runnable r, long delayedSeconds) {
        // 包装成时间轮识别的任务对象
        TimerTask task = timeout -> r.run();
        // 添加到时间轮,并设置执行的延迟时间
        wheelTimer.newTimeout(task, delayedSeconds, TimeUnit.SECONDS);
    }
    
  • 添加一个周期执行的任务
    // 定义一个时间轮
    private HashedWheelTimer wheelTimer = new HashedWheelTimer();
    
    public void scheduleTaskWithFixedDelay(Runnable r, long delayedSeconds) {
        // 包装成时间轮识别的任务对象
        TimerTask task = timeout -> {
            try {
                // do your business
                r.run();
            } catch (Exception e) {
                // catch your exception
            } finally {
                // 最后重新再次添加到时间轮,并制定同样的时间作为下次执行的时间
                wheelTimer.newTimeout(timeout.task(), delayedSeconds, TimeUnit.SECONDS);
            }
        };
        // 添加到时间轮,并设置执行的延迟时间
        wheelTimer.newTimeout(task, delayedSeconds, TimeUnit.SECONDS);
    }
    

4 为什么时间轮是单线程调度

要解释这个问题,需要搞清楚时间轮的实现原理。

4.1 时间轮的实现原理

前文提到时间轮的核心就两个资源:

  1. 一个负责调度并执行任务的线程
  2. 存储任务的线性数组

很简单纯粹,但时间轮是如何实现通过一个数组来存储不同时间点触发的任务的呢,然后那个“单线程”又是怎样去扫描判断然后调度的呢?这是需要研究源码原理的,但是不着急,在此之前,先从上帝视角来看一下时间轮的实现原理。

4.1.1 时间轮的业务架构

image.png

这显然是一个时钟,我们都知道时钟表盘按秒钟有60个格子,秒针每走一下是1秒,会移动一个格子,走完60秒后会从头开始。

image.png

所谓“时间轮”是基于线性数组来实现的,之所以说“轮”,是为了便于形象化地让人理解。可以将该数组比作上图中的时钟表盘,数组可以理解成这“60”个格子,线程每次检查则视为往前走一个,指向数组中的一个元素,检查是否满足可执行的时间条件,满足则执行,执行完毕后继续往下走。

4.1.2 数组长度是有限的,如何存储2W个数据呢

可以看到上面的业务架构图中,数组中的每一个元素,实际上往下延伸是一种链表结构,这一点可以类比HashMap中的结构,这样就比较好理解了。

这样设计的目的,是为了引入一个“圈次”的概念,以此来减少单线程每次轮询任务的时间

想象一下,假如我真的有2w个任务都添加到时间轮数组里,那我需要将该数组的size设置为2w,这样一来,我的时钟表盘就有2w个格子,我的单线程每次轮询都要走2w次才完成一次检查。

假如现在时间是18:00:00,首个任务执行时间是18:00:02,我的线程刚好check到第二个任务,我还有19998个任务需要check,我很难在2s内完成这么多任务的check,当我的线程转完一圈回来从头再开始check时,已经18:00:05了。好家伙,我慢了足足3s,不仅如此,后面的任务都会延迟,这...

那怎么解决这个问题呢?就是通过“圈次”来解决。

我就将我的数组size设置一个固定的值,比如就是8/16/32,最多不超过100,以此来控制每圈check的时间。然后将这2w个任务,按照每一圈的size分成好几批,按照延迟的时间大小,以此往后排。就像这张图所示的样子:

image.png

虽然可能存在很多个圈次,但实际上并不需要给每个圈次都创建一个数组,没错,本质上还是同一个数组,假如当我只设置了size大小为32时,如果添加了2w个任务数据,超过的数据会通过链表的方式添加到下一个圈次。这样就能够使用最少的内存资源,完成最大限度的资源存储。

5 时间轮的实现原理

前文提到了好几个概念,其实时间轮中一共包含以下几个要素:

  • 负责check和执行的线程,可以类比时钟的秒针辅助理解
  • 负责存储任务的数组大小,可以类比时钟的格子辅助理解
  • 圈次,基于链表进行实现
  • 线程tick的时间间隔,可以类比时钟秒针每走一秒需要的时间

下面以netty中的HashedWheelTimer为例,基于源码进行分析

5.1 时间轮的构造函数

/**
 * Creates a new timer.
 *
 * @param threadFactory        a {@link ThreadFactory} that creates a
 *                             background {@link Thread} which is dedicated to
 *                             {@link TimerTask} execution.
 * @param tickDuration         the duration between tick
 * @param unit                 the time unit of the {@code tickDuration}
 * @param ticksPerWheel        the size of the wheel
 * @param leakDetection        {@code true} if leak detection should be enabled always,
 *                             if false it will only be enabled if the worker thread is not
 *                             a daemon thread.
 * @param  maxPendingTimeouts  The maximum number of pending timeouts after which call to
 *                             {@code newTimeout} will result in
 *                             {@link java.util.concurrent.RejectedExecutionException}
 *                             being thrown. No maximum pending timeouts limit is assumed if
 *                             this value is 0 or negative.
 * @throws NullPointerException     if either of {@code threadFactory} and {@code unit} is {@code null}
 * @throws IllegalArgumentException if either of {@code tickDuration} and {@code ticksPerWheel} is <= 0
 */
public HashedWheelTimer(
        ThreadFactory threadFactory,
        long tickDuration, TimeUnit unit, int ticksPerWheel, boolean leakDetection,
        long maxPendingTimeouts) {

    if (threadFactory == null) {
        throw new NullPointerException("threadFactory");
    }
    if (unit == null) {
        throw new NullPointerException("unit");
    }
    if (tickDuration <= 0) {
        throw new IllegalArgumentException("tickDuration must be greater than 0: " + tickDuration);
    }
    if (ticksPerWheel <= 0) {
        throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel);
    }

    // Normalize ticksPerWheel to power of two and initialize the wheel.
    wheel = createWheel(ticksPerWheel);
    mask = wheel.length - 1;

    // Convert tickDuration to nanos.
    long duration = unit.toNanos(tickDuration);

    // Prevent overflow.
    if (duration >= Long.MAX_VALUE / wheel.length) {
        throw new IllegalArgumentException(String.format(
                "tickDuration: %d (expected: 0 < tickDuration in nanos < %d",
                tickDuration, Long.MAX_VALUE / wheel.length));
    }

    if (duration < MILLISECOND_NANOS) {
        if (logger.isWarnEnabled()) {
            logger.warn("Configured tickDuration %d smaller then %d, using 1ms.",
                        tickDuration, MILLISECOND_NANOS);
        }
        this.tickDuration = MILLISECOND_NANOS;
    } else {
        this.tickDuration = duration;
    }

    workerThread = threadFactory.newThread(worker);

    leak = leakDetection || !workerThread.isDaemon() ? leakDetector.track(this) : null;

    this.maxPendingTimeouts = maxPendingTimeouts;

    if (INSTANCE_COUNTER.incrementAndGet() > INSTANCE_COUNT_LIMIT &&
        WARNED_TOO_MANY_INSTANCES.compareAndSet(false, true)) {
        reportTooManyInstances();
    }
}

一般的时间轮中,负责check和执行任务的线程是单线程,往往是不需要我们维护的,随着时间轮的创建而创建(最多基于ThreadFactory指定一下线程的名称等参数,此处不做过多解释)。也不需要关心“圈次”,因为它是默认基于链表维护的,我们需要关心的只有两个参数:

  1. 数组大小

    对应的是上述源码中的 ticksPerWheel 参数,表示的是每一圈的tick次数

  2. 线程每次tick的时间

    对应的是上述源码中的 tickDuration 参数,表示的是每一次tick需要的延时

在HashedWheelTimer中,如果使用无参构造函数创建该时间轮,默认是这样:

public HashedWheelTimer(ThreadFactory threadFactory) {
    this(threadFactory, 100, TimeUnit.MILLISECONDS);
}

public HashedWheelTimer(
        ThreadFactory threadFactory, long tickDuration, TimeUnit unit) {
    this(threadFactory, tickDuration, unit, 512);
}

tickDuration为100;ticksPerWheel为512 也就是说,数组size大小默认为512,线程每次check的时间间隔为100ms

在构造完成后,时间轮会初始化数组,以及相关成员变量,但此时并不会立刻启动线程开始check

5.2 添加任务

添加任务的函数是 newTimeoout

@Override
public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
    if (task == null) {
        throw new NullPointerException("task");
    }
    if (unit == null) {
        throw new NullPointerException("unit");
    }

    long pendingTimeoutsCount = pendingTimeouts.incrementAndGet();

    if (maxPendingTimeouts > 0 && pendingTimeoutsCount > maxPendingTimeouts) {
        pendingTimeouts.decrementAndGet();
        throw new RejectedExecutionException("Number of pending timeouts ("
            + pendingTimeoutsCount + ") is greater than or equal to maximum allowed pending "
            + "timeouts (" + maxPendingTimeouts + ")");
    }
    // 开启线程check的方法
    start();

    // Add the timeout to the timeout queue which will be processed on the next tick.
    // During processing all the queued HashedWheelTimeouts will be added to the correct HashedWheelBucket.
    long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;

    // Guard against overflow.
    if (delay > 0 && deadline < 0) {
        deadline = Long.MAX_VALUE;
    }
    HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
    timeouts.add(timeout);
    return timeout;
}

会在这里构建一个HashedWheelTimeout,这是一个内部类,表示该时间轮中封装的可执行的任务:

private static final class HashedWheelTimeout implements Timeout {

    private static final int ST_INIT = 0;
    private static final int ST_CANCELLED = 1;
    private static final int ST_EXPIRED = 2;
    private static final AtomicIntegerFieldUpdater<HashedWheelTimeout> STATE_UPDATER =
            AtomicIntegerFieldUpdater.newUpdater(HashedWheelTimeout.class, "state");

    private final HashedWheelTimer timer;
    private final TimerTask task;
    private final long deadline;

    @SuppressWarnings({"unused", "FieldMayBeFinal", "RedundantFieldInitialization" })
    private volatile int state = ST_INIT;

    // remainingRounds will be calculated and set by Worker.transferTimeoutsToBuckets() before the
    // HashedWheelTimeout will be added to the correct HashedWheelBucket.
    long remainingRounds;

    // This will be used to chain timeouts in HashedWheelTimerBucket via a double-linked-list.
    // As only the workerThread will act on it there is no need for synchronization / volatile.
    HashedWheelTimeout next;
    HashedWheelTimeout prev;

    // The bucket to which the timeout was added
    HashedWheelBucket bucket;

    HashedWheelTimeout(HashedWheelTimer timer, TimerTask task, long deadline) {
        this.timer = timer;
        this.task = task;
        this.deadline = deadline;
    }

    @Override
    public Timer timer() {
        return timer;
    }

    @Override
    public TimerTask task() {
        return task;
    }

    @Override
    public boolean cancel() {
        // only update the state it will be removed from HashedWheelBucket on next tick.
        if (!compareAndSetState(ST_INIT, ST_CANCELLED)) {
            return false;
        }
        // If a task should be canceled we put this to another queue which will be processed on each tick.
        // So this means that we will have a GC latency of max. 1 tick duration which is good enough. This way
        // we can make again use of our MpscLinkedQueue and so minimize the locking / overhead as much as possible.
        timer.cancelledTimeouts.add(this);
        return true;
    }

    void remove() {
        HashedWheelBucket bucket = this.bucket;
        if (bucket != null) {
            bucket.remove(this);
        } else {
            timer.pendingTimeouts.decrementAndGet();
        }
    }

    public boolean compareAndSetState(int expected, int state) {
        return STATE_UPDATER.compareAndSet(this, expected, state);
    }

    public int state() {
        return state;
    }

    @Override
    public boolean isCancelled() {
        return state() == ST_CANCELLED;
    }

    @Override
    public boolean isExpired() {
        return state() == ST_EXPIRED;
    }

    public void expire() {
        if (!compareAndSetState(ST_INIT, ST_EXPIRED)) {
            return;
        }

        try {
            task.run(this);
        } catch (Throwable t) {
            if (logger.isWarnEnabled()) {
                logger.warn("An exception was thrown by " + TimerTask.class.getSimpleName() + '.', t);
            }
        }
    }

    @Override
    public String toString() {
        final long currentTime = System.nanoTime();
        long remaining = deadline - currentTime + timer.startTime;

        StringBuilder buf = new StringBuilder(192)
           .append(simpleClassName(this))
           .append('(')
           .append("deadline: ");
        if (remaining > 0) {
            buf.append(remaining)
               .append(" ns later");
        } else if (remaining < 0) {
            buf.append(-remaining)
               .append(" ns ago");
        } else {
            buf.append("now");
        }

        if (isCancelled()) {
            buf.append(", cancelled");
        }

        return buf.append(", task: ")
                  .append(task())
                  .append(')')
                  .toString();
    }
}

而start方法则是启动线程的方法,会通过cas进行原子性判断,只会执行一次,如果已启动则不会启动,这里是一个懒加载的设计,防止业务侧未添加任务进行cpu空转。

/**
 * Starts the background thread explicitly.  The background thread will
 * start automatically on demand even if you did not call this method.
 *
 * @throws IllegalStateException if this timer has been
 *                               {@linkplain #stop() stopped} already
 */
public void start() {
    switch (WORKER_STATE_UPDATER.get(this)) {
        case WORKER_STATE_INIT:
            if (WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_INIT, WORKER_STATE_STARTED)) {
                workerThread.start();
            }
            break;
        case WORKER_STATE_STARTED:
            break;
        case WORKER_STATE_SHUTDOWN:
            throw new IllegalStateException("cannot be started once stopped");
        default:
            throw new Error("Invalid WorkerState");
    }

    // Wait until the startTime is initialized by the worker.
    while (startTime == 0) {
        try {
            startTimeInitialized.await();
        } catch (InterruptedException ignore) {
            // Ignore - it will be ready very soon.
        }
    }
}

5.3 时间轮的tick

真正check并执行任务的函数,在Worker对象中,回头看一眼HashedWheelTimer中的构造函数,里面有一段这样的代码:

workerThread = threadFactory.newThread(worker);

这里的worker,看成员变量,是一个Worker对象

private final Worker worker = new Worker();
private final class Worker implements Runnable {
    private final Set<Timeout> unprocessedTimeouts = new HashSet<Timeout>();

    private long tick;

    @Override
    public void run() {
        // Initialize the startTime.
        startTime = System.nanoTime();
        if (startTime == 0) {
            // We use 0 as an indicator for the uninitialized value here, so make sure it's not 0 when initialized.
            startTime = 1;
        }

        // Notify the other threads waiting for the initialization at start().
        startTimeInitialized.countDown();

        do {
            final long deadline = waitForNextTick();
            if (deadline > 0) {
                int idx = (int) (tick & mask);
                processCancelledTasks();
                HashedWheelBucket bucket =
                        wheel[idx];
                transferTimeoutsToBuckets();
                bucket.expireTimeouts(deadline);
                tick++;
            }
        } while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);

        // Fill the unprocessedTimeouts so we can return them from stop() method.
        for (HashedWheelBucket bucket: wheel) {
            bucket.clearTimeouts(unprocessedTimeouts);
        }
        for (;;) {
            HashedWheelTimeout timeout = timeouts.poll();
            if (timeout == null) {
                break;
            }
            if (!timeout.isCancelled()) {
                unprocessedTimeouts.add(timeout);
            }
        }
        processCancelledTasks();
    }

    private void transferTimeoutsToBuckets() {
        // transfer only max. 100000 timeouts per tick to prevent a thread to stale the workerThread when it just
        // adds new timeouts in a loop.
        for (int i = 0; i < 100000; i++) {
            HashedWheelTimeout timeout = timeouts.poll();
            if (timeout == null) {
                // all processed
                break;
            }
            if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) {
                // Was cancelled in the meantime.
                continue;
            }

            long calculated = timeout.deadline / tickDuration;
            timeout.remainingRounds = (calculated - tick) / wheel.length;

            final long ticks = Math.max(calculated, tick); // Ensure we don't schedule for past.
            int stopIndex = (int) (ticks & mask);

            HashedWheelBucket bucket = wheel[stopIndex];
            bucket.addTimeout(timeout);
        }
    }

    private void processCancelledTasks() {
        for (;;) {
            HashedWheelTimeout timeout = cancelledTimeouts.poll();
            if (timeout == null) {
                // all processed
                break;
            }
            try {
                timeout.remove();
            } catch (Throwable t) {
                if (logger.isWarnEnabled()) {
                    logger.warn("An exception was thrown while process a cancellation task", t);
                }
            }
        }
    }

    /**
     * calculate goal nanoTime from startTime and current tick number,
     * then wait until that goal has been reached.
     * @return Long.MIN_VALUE if received a shutdown request,
     * current time otherwise (with Long.MIN_VALUE changed by +1)
     */
    private long waitForNextTick() {
        long deadline = tickDuration * (tick + 1);

        for (;;) {
            final long currentTime = System.nanoTime() - startTime;
            long sleepTimeMs = (deadline - currentTime + 999999) / 1000000;

            if (sleepTimeMs <= 0) {
                if (currentTime == Long.MIN_VALUE) {
                    return -Long.MAX_VALUE;
                } else {
                    return currentTime;
                }
            }

            // Check if we run on windows, as if thats the case we will need
            // to round the sleepTime as workaround for a bug that only affect
            // the JVM if it runs on windows.
            //
            // See https://github.com/netty/netty/issues/356
            if (PlatformDependent.isWindows()) {
                sleepTimeMs = sleepTimeMs / 10 * 10;
            }

            try {
                Thread.sleep(sleepTimeMs);
            } catch (InterruptedException ignored) {
                if (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_SHUTDOWN) {
                    return Long.MIN_VALUE;
                }
            }
        }
    }

    public Set<Timeout> unprocessedTimeouts() {
        return Collections.unmodifiableSet(unprocessedTimeouts);
    }
}

可以发现,Worker毫无意外地实现了Runnable,所以核心在其中的run方法,而其中最最核心的源码,在于这个do while循环:

do {
    final long deadline = waitForNextTick();
    if (deadline > 0) {
        int idx = (int) (tick & mask);
        processCancelledTasks();
        HashedWheelBucket bucket =
                wheel[idx];
        transferTimeoutsToBuckets();
        bucket.expireTimeouts(deadline);
        tick++;
    }
} while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);

通过tick 与操作,与上掩码mask(值在构造函数中已定义,为:wheel.length - 1,也就是设置的数组size - 1),这样做是为了保证每次tick的index在数组圈定范围内,每次+1。随后获取指定index的元素。判断是否满足时间条件,如果有多个任务满足同一个条件,则基于链表逐个获取执行,完成后执行后续的tick。

6 时间轮有什么好处和坏处呢

好处当然是节省系统资源。

系统cpu资源和内存资源在如今的业务环境中是比较宝贵的,如果能够基于单线程来调度成吨的延时任务,并且还能满足业务,我相信你的组长肯定会对你刮目相看,你的老板说不定也会因为你这个操作节省了一波买服务器的开销给你多发100块的年终奖。

那坏处呢?

你可以发现,至少netty的这个时间轮框架,是完全基于内存的,因此存在以下几个问题:

  1. 系统异常重启后,时间轮中的任务会丢失
  2. 没有提供重复执行任务的api

    解决方案在前面有demo样例,通过手动重复添加即可实现

  3. 单线程可能因前序任务执行任务过长导致后续任务比原计划时间点延迟

6.1 如何弥补时间轮中的不足之处呢

基于上述缺点,针对性地给出如下解决方案:

  1. 可自行根据业务涉及任务表,并维护相关参数(时间点,是否周期性执行等),在服务重启时查询数据库,基于参数再重新添加到时间轮
  2. 解决方案在前面有demo样例,通过手动重复添加即可实现
  3. 这就要根据业务进行权衡,技术侧没有银弹来解决全部的业务场景的问题

当然,时间轮的框架多种多样,有的框架提供了丰富的api,但这都是基于易用性和性能的权衡。当然屏幕前的你也可以手写一个自己的时间轮,来定制化地满足自己的业务场景,达到易用性和性能的最佳权衡。

毕竟,核心点就只有两个:

  • 线程的调度
  • 数组的维护