java 时间轮算法_时间轮算法(TimingWheel)是如何实现的?

article/2025/7/28 0:59:06

前言

我在2. SOFAJRaft源码分析—JRaft的定时任务调度器是怎么做的?这篇文章里已经讲解过时间轮算法在JRaft中是怎么应用的,但是我感觉我并没有讲解清楚这个东西,导致看了这篇文章依然和没看是一样的,所以我打算重新说透时间轮算法。

时间轮的应用并非 JRaft 独有,其应用场景还有很多,在 Netty、Akka、Quartz、ZooKeeper 、Kafka等组件中都存在时间轮的踪影。

我们下面讲解的时间轮的实现以JRaft中的为例子进行讲解,因为JRaft这部分的代码是参考Netty的,所以大家也可以去Netty中去寻找源码实现。

时间轮用来解决什么问题?

如果一个系统中存在着大量的调度任务,而大量的调度任务如果每一个都使用自己的调度器来管理任务的生命周期的话,浪费cpu的资源并且很低效。

时间轮是一种高效来利用线程资源来进行批量化调度的一种调度模型。把大批量的调度任务全部都绑定到同一个的调度器上面,使用这一个调度器来进行所有任务的管理(manager),触发(trigger)以及运行(runnable)。能够高效的管理各种延时任务,周期任务,通知任务等等。

不过,时间轮调度器的时间精度可能不是很高,对于精度要求特别高的调度任务可能不太适合。因为时间轮算法的精度取决于,时间段“指针”单元的最小粒度大小,比如时间轮的格子是一秒跳一次,那么调度精度小于一秒的任务就无法被时间轮所调度。

时间轮结构

8693ef8b29c301eeb9cb7f05de9fc5b9.png

如图,JRaft中时间轮(HashedWheelTimer)是一个存储定时任务的环形队列,底层采用数组实现,数组中的每个元素可以存放一个定时任务列表(HashedWheelBucket),HashedWheelBucket是一个环形的双向链表,链表中的每一项表示的都是定时任务项(HashedWheelTimeout),其中封装了真正的定时任务(TimerTask)。

时间轮由多个时间格组成,每个时间格代表当前时间轮的基本时间跨度(tickDuration)。时间轮的时间格个数是固定的,可用 wheel.length 来表示。

时间轮还有一个表盘指针(tick),用来表示时间轮当前指针跳动的次数,可以用tickDuration * (tick + 1)来表示下一次到期的任务,需要处理此时间格所对应的 HashedWheelBucket 中的所有任务。

时间轮运行逻辑

时间轮在启动的时候会记录一下当前启动的时间赋值给startTime。时间轮在添加任务的时候首先会计算延迟时间(deadline),比如一个任务的延迟时间为24ms,那么会将当前的时间(currentTime)+24ms-时间轮启动时的时间(startTime)。然后将任务封装成HashedWheelTimeout加入到timeouts队列中,作为缓存。

时间轮在运行的时候会将timeouts中缓存的HashedWheelTimeout任务取10万个出来进行遍历。

然后需要计算出几个参数值:

HashedWheelTimeout的总共延迟的次数:将每个任务的延迟时间(deadline)/tickDuration 计算出tick需要总共跳动的次数;

计算时间轮round次数:根据计算的需要走的(总次数- 当前tick数量)/ 时间格个数(wheel.length)。比如tickDuration为1ms,时间格个数为20个,那么时间轮走一圈需要20ms,那么添加进一个延时为24ms的数据,如果当前的tick为0,那么计算出的轮数为1,指针没运行一圈就会将round取出来减一,所以需要转动到第二轮之后才可以将轮数round减为0之后才会运行

计算出该任务需要放置到时间轮(wheel)的槽位,然后加入到槽位链表最后

将timeouts中的数据放置到时间轮wheel中之后,计算出当前时针走到的槽位的位置,并取出槽位中的链表数据,将deadline和当前的时间做对比,运行过期的数据。

源码分析

构造器

public HashedWheelTimer(ThreadFactory threadFactory, long tickDuration, TimeUnit unit, int ticksPerWheel,

long maxPendingTimeouts) {

if (threadFactory == null) {

throw new NullPointerException("threadFactory");

}

//unit = MILLISECONDS

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.

// 创建一个HashedWheelBucket数组

// 创建时间轮基本的数据结构,一个数组。长度为不小于ticksPerWheel的最小2的n次方

wheel = createWheel(ticksPerWheel);

// 这是一个标示符,用来快速计算任务应该呆的格子。

// 我们知道,给定一个deadline的定时任务,其应该呆的格子=deadline%wheel.length.但是%操作是个相对耗时的操作,所以使用一种变通的位运算代替:

// 因为一圈的长度为2的n次方,mask = 2^n-1后低位将全部是1,然后deadline&mast == deadline%wheel.length

// java中的HashMap在进行hash之后,进行index的hash寻址寻址的算法也是和这个一样的

mask = wheel.length - 1;

// Convert tickDuration to nanos.

//tickDuration传入是1的话,这里会转换成1000000

this.tickDuration = unit.toNanos(tickDuration);

// Prevent overflow.

// 校验是否存在溢出。即指针转动的时间间隔不能太长而导致tickDuration*wheel.length>Long.MAX_VALUE

if (this.tickDuration >= Long.MAX_VALUE / wheel.length) {

throw new IllegalArgumentException(String.format(

"tickDuration: %d (expected: 0 < tickDuration in nanos < %d", tickDuration, Long.MAX_VALUE

/ wheel.length));

}

//将worker包装成thread

workerThread = threadFactory.newThread(worker);

//maxPendingTimeouts = -1

this.maxPendingTimeouts = maxPendingTimeouts;

//如果HashedWheelTimer实例太多,那么就会打印一个error日志

if (instanceCounter.incrementAndGet() > INSTANCE_COUNT_LIMIT

&& warnedTooManyInstances.compareAndSet(false, true)) {

reportTooManyInstances();

}

}

在这个构造器中有几个细节需要注意:

调用createWheel方法创建的wheel数组一定是2次方数,比如传入的ticksPerWheel是6,那么初始化的wheel长度一定是8。这样做是为了让mask & tick 来计算出槽位

tickDuration用的是纳秒

在构造里面并不会里面启动时间轮,而是要等到有第一个任务加入到时间轮的时候才启动。在构造器里面会将工作线程worker封装成workerThread

放入任务到时间轮中

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 + ")");

}

// 如果时间轮没有启动,则启动

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.

//在delay为正数的情况下,deadline是不可能为负数

//如果为负数,那么说明超过了long的最大值

if (delay > 0 && deadline < 0) {

deadline = Long.MAX_VALUE;

}

// 这里定时任务不是直接加到对应的格子中,而是先加入到一个队列里,然后等到下一个tick的时候,

// 会从队列里取出最多100000个任务加入到指定的格子中

HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);

//Worker会去处理timeouts队列里面的数据

timeouts.add(timeout);

return timeout;

}

如果时间轮没有启动,那么就调用start方法启动时间轮,启动时间轮之后会为startTime设置为当前时间

计算延迟时间deadline

将task任务封装到HashedWheelTimeout中,然后添加到timeouts队列中进行缓存

start

private final CountDownLatch startTimeInitialized = new CountDownLatch(1);

public void start() {

//workerState一开始的时候是0(WORKER_STATE_INIT),然后才会设置为1(WORKER_STATE_STARTED)

switch (workerStateUpdater.get(this)) {

case WORKER_STATE_INIT:

//使用cas来获取启动调度的权力,只有竞争到的线程允许来进行实例启动

if (workerStateUpdater.compareAndSet(this, WORKER_STATE_INIT, WORKER_STATE_STARTED)) {

//如果成功设置了workerState,那么就调用workerThread线程

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");

}

// 等待worker线程初始化时间轮的启动时间

// Wait until the startTime is initialized by the worker.

while (startTime == 0) {

try {

//这里使用countDownLauch来确保调度的线程已经被启动

startTimeInitialized.await();

} catch (InterruptedException ignore) {

// Ignore - it will be ready very soon.

}

}

}

start方法会根据当前的workerState状态来启动时间轮。并且用了startTimeInitialized来控制线程的运行,如果workerThread没有启动起来,那么newTimeout方法会一直阻塞在运行start方法中。如果不阻塞,newTimeout方法会获取不到startTime。

启动时间轮

时间轮的启动在HashedWheelTimer的内部类Worker中。调用workerThread#start方法会调用Worker的run方法启动时间轮。

下面我们看时间轮启动做了什么,下面的分析不考虑任务被取消的情况。

Worker#run

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;

}

//HashedWheelTimer的start方法会继续往下运行

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

startTimeInitialized.countDown();

do {

//返回的是当前的nanoTime- startTime

//也就是返回的是 每 tick 一次的时间间隔

final long deadline = waitForNextTick();

if (deadline > 0) {

//算出时间轮的槽位

int idx = (int) (tick & mask);

//移除cancelledTimeouts中的bucket

// 从bucket中移除timeout

processCancelledTasks();

HashedWheelBucket bucket = wheel[idx];

// 将newTimeout()方法中加入到待处理定时任务队列中的任务加入到指定的格子中

transferTimeoutsToBuckets();

bucket.expireTimeouts(deadline);

tick++;

}

// 校验如果workerState是started状态,那么就一直循环

} while (workerStateUpdater.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;

}

//如果有没有被处理的timeout,那么加入到unprocessedTimeouts对列中

if (!timeout.isCancelled()) {

unprocessedTimeouts.add(timeout);

}

}

//处理被取消的任务

processCancelledTasks();

}

时间轮运行的时候首先会记录一下启动时间(startTime),然后调用startTimeInitialized释放外层的等待线程;

进入dowhile循环,调用waitForNextTick睡眠等待到下一次的tick指针的跳动,并返回当前时间减去startTime作为deadline

由于mask= wheel.length -1 ,wheel是2的次方数,所以可以直接用tick & mask 计算出此次在wheel中的槽位

调用processCancelledTasks将cancelledTimeouts队列中的任务取出来,并将当前的任务从时间轮中移除

调用transferTimeoutsToBuckets方法将timeouts队列中缓存的数据取出加入到时间轮中

运行目前指针指向的槽位中的bucket链表数据

时间轮指针跳动

waitForNextTick

//sleep, 直到下次tick到来, 然后返回该次tick和启动时间之间的时长

private long waitForNextTick() {

//tickDuration这里是100000

//tick表示总tick数

long deadline = tickDuration * (tick + 1);

for (;;) {

final long currentTime = System.nanoTime() - startTime;

// 计算需要sleep的时间, 之所以加999999后再除10000000,前面是1所以这里需要减去1,

// 才能计算准确,还有通过这里可以看到 其实线程是以睡眠一定的时候再来执行下一个ticket的任务的,

//这样如果ticket的间隔设置的太小的话,系统会频繁的睡眠然后启动,

//其实感觉影响部分的性能,所以为了更好的利用系统资源步长可以稍微设置大点

long sleepTimeMs = (deadline - currentTime + 999999) / 1000000;

//sleepTimeMs小于零表示走到了下一个时间轮位置

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 (Platform.isWindows()) {

sleepTimeMs = sleepTimeMs / 10 * 10;

}

try {

Thread.sleep(sleepTimeMs);

} catch (InterruptedException ignored) {

if (workerStateUpdater.get(HashedWheelTimer.this) == WORKER_STATE_SHUTDOWN) {

return Long.MIN_VALUE;

}

}

}

}

可以想象一下在时钟的秒钟上面秒与秒之间的时间是需要等待的,那么waitForNextTick这个方法就是根据当前的时间计算出跳动到下个时间的间隔时间,并进行sleep操作,然后返回当前时间距离时间轮启动时间的时间段。

转移任务到时间轮中

在调用时间轮的方法加入任务的时候并没有直接加入到时间轮中,而是缓存到了timeouts队列中,所以在运行的时候需要将timeouts队列中的任务转移到时间轮数据的链表中

transferTimeoutsToBuckets

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.

// 每次tick只处理10w个任务,以免阻塞worker线程

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;

}

//calculated = tick 次数

long calculated = timeout.deadline / tickDuration;

// 计算剩余的轮数, 只有 timer 走够轮数, 并且到达了 task 所在的 slot, task 才会过期

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

//如果任务在timeouts队列里面放久了, 以至于已经过了执行时间, 这个时候就使用当前tick, 也就是放到当前bucket, 此方法调用完后就会被执行

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

算出任务应该插入的 wheel 的 slot, slotIndex = tick 次数 & mask, mask = wheel.length - 1

int stopIndex = (int) (ticks & mask);

HashedWheelBucket bucket = wheel[stopIndex];

//将timeout加入到bucket链表中

bucket.addTimeout(timeout);

}

}

在这个转移方法中,写死了一个循环,每次都只转移10万个任务。

然后根据HashedWheelTimeout的deadline延迟时间计算出时间轮需要运行多少次才能运行当前的任务,如果当前的任务延迟时间大于时间轮跑一圈所需要的时间,那么就计算需要跑几圈才能到这个任务运行。

最后计算出该任务在时间轮中的槽位,添加到时间轮的链表中。

运行时间轮中的任务

当指针跳到时间轮的槽位的时间,会将槽位的HashedWheelBucket取出来,然后遍历链表,运行其中到期的任务。

expireTimeouts

// 过期并执行格子中的到期任务,tick到该格子的时候,worker线程会调用这个方法

//根据deadline和remainingRounds判断任务是否过期

public void expireTimeouts(long deadline) {

HashedWheelTimeout timeout = head;

// process all timeouts

//遍历格子中的所有定时任务

while (timeout != null) {

// 先保存next,因为移除后next将被设置为null

HashedWheelTimeout next = timeout.next;

if (timeout.remainingRounds <= 0) {

//从bucket链表中移除当前timeout,并返回链表中下一个timeout

next = remove(timeout);

//如果timeout的时间小于当前的时间,那么就调用expire执行task

if (timeout.deadline <= deadline) {

timeout.expire();

} else {

//不可能发生的情况,就是说round已经为0了,deadline却>当前槽的deadline

// The timeout was placed into a wrong slot. This should never happen.

throw new IllegalStateException(String.format("timeout.deadline (%d) > deadline (%d)",

timeout.deadline, deadline));

}

} else if (timeout.isCancelled()) {

next = remove(timeout);

} else {

//因为当前的槽位已经过了,说明已经走了一圈了,把轮数减一

timeout.remainingRounds--;

}

//把指针放置到下一个timeout

timeout = next;

}

}

HashedWheelBucket是一个链表,所以我们需要从head节点往下进行遍历。如果链表没有遍历到链表尾部那么就继续往下遍历。

获取的timeout节点节点,如果剩余轮数remainingRounds大于0,那么就说明要到下一圈才能运行,所以将剩余轮数减一;

如果当前剩余轮数小于等于零了,那么就将当前节点从bucket链表中移除,并判断一下当前的时间是否大于timeout的延迟时间,如果是则调用timeout的expire执行任务。


http://chatgpt.dhexx.cn/article/EafBEx50.shtml

相关文章

Redis之时间轮机制(五)

&#x1f680; 优质资源分享 &#x1f680; 学习路线指引&#xff08;点击解锁&#xff09;知识定位人群定位&#x1f9e1; Python实战微信订餐小程序 &#x1f9e1;进阶级本课程是python flask微信小程序的完美结合&#xff0c;从项目搭建到腾讯云部署上线&#xff0c;打造一…

C++定时器和时间轮

文章目录 定时器最小堆实现定时器 时间轮单层级时间轮多层级时间轮 定时器 有些时候我们需要延迟执行一些功能&#xff0c;比如每10s进行一次数据采集。或者告知用户技能冷却有多少时间&#xff0c;如果我们将执行这些功能的任务交给主线程&#xff0c;就会造成主线程的阻塞。…

简单描述时间轮

时间轮 作用 也是用来作定时器触发任务&#xff0c;只是他更高效&#xff0c;时间复杂度为O(1)。 运行原理 为了方便理解我们参考钟表&#xff0c;它分为3个层次&#xff1a;时、分、秒&#xff0c;只有秒针在运动&#xff0c;走动一格时间为1秒&#xff0c;走一圈为1分钟&…

再谈时间轮

时间轮很早前就很流行了&#xff0c;在很多优秀开源框架中都有用到&#xff0c;像kafka、netty。也算是现在工程师基本都了解的一个知识储备了。有幸在工作中造过两次轮子&#xff0c;所以今天聊聊时间轮。 时间轮是一种高性能定时器。 时间轮&#xff0c;顾名思义&#xff0c…

时间轮原理及其在框架中的应用

一、时间轮简介 1.1 为什么要使用时间轮 在平时开发中&#xff0c;经常会与定时任务打交道。下面举几个定时任务处理的例子。 1&#xff09;心跳检测。在Dubbo中&#xff0c;需要有心跳机制来维持Consumer与Provider的长连接&#xff0c;默认的心跳间隔是60s。当Provider在3…

时间轮分析

背景 在需要完成重试机制或者心跳机制一类的业务&#xff0c;除了使用excutors做定时任务。还可以使用时间轮完成这类需求 分析 使用dubbo中的心跳重试作为案例分析时间轮的使用 源码分析 在HeaderExchangeClient启动的时候调用startHeartBeatTask()开启心跳任务&#xff0…

Netty时间轮

概述 时间轮是一个高性能&#xff0c;低消耗的数据结构&#xff0c;它适合用非准实时&#xff0c;延迟的短平快任务&#xff0c;例如心跳检测。在netty和kafka中都有使用。 比如Netty动辄管理100w的连接&#xff0c;每一个连接都会有很多超时任务。比如发送超时、心跳检测间隔等…

高性能定时器时间轮的实现

时间轮的概念 关于定时器有很多种&#xff0c;有基于升序的定时器时间链表&#xff0c;但是这种链表存在效率的不足&#xff0c;就是当插入定时器的时候时间复杂度是O(n).今天&#xff0c;我们来认识一下高性能定时器时间轮。 如上图所示&#xff0c;就是一个时间轮的基本轮廓…

深度剖析 -- 时间轮算法(TimingWheel)是如何实现的?

前言 时间轮的应用场景很多&#xff0c;在 Netty、Akka、Quartz、ZooKeeper 、Kafka等组件中都存在时间轮的踪影。我们下面讲解的时间轮的实现以JRaft中的为例子进行讲解&#xff0c;因为JRaft这部分的代码是参考Netty的&#xff0c;所以大家也可以去Netty中去寻找源码实现。 …

Kafka原理--时间轮(延时操作)

原文网址&#xff1a;Kafka原理--时间轮(延时操作)_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍Kafka的时间轮的原理。 Kafka没有延迟队列功能供用户使用&#xff0c;本文介绍的延时操作仅仅是Kafka内部的使用&#xff0c;用户无法使用。 Kafka延时操作 Kafka中存在大量的…

时间轮算法HashedWheelTimer

文章目录 一.HashedWheelTimer是什么?二.能干什么?为什么需要这个东西?优点适用场景 三.怎么用?使用步骤1.引入pom2.使用举例 四.时间轮原理五.使用注意点1.一个HashedWheelTimer对象只有一个worker线程2.每次添加的任务只会执行一次3.时间轮的参数非常重要4.所有的任务都是…

时间轮-理论篇

1. 引言 定时任务再开发过程中无处不在&#xff0c;定时发送消息&#xff0c;定时更新数据库表的状态&#xff0c;Linux系统定时执行脚本等等。这些操作都离不开定时任务&#xff0c;那么这些定时任务是怎么实现的是否又去想过。如果让开发者自己去实现一个定时任务又该怎么实…

定时任务的实现原理:时间轮算法

文章目录 前言时间轮定时使用方式时间轮定时内部原理时间轮定时源码剖析构造方法添加任务工作线程启动工作线程run方法指针跳动将队列任务放入时间轮中链表任务遍历定时任务执行 前言 最近在思考实现定时任务的几种方式&#xff0c;比如 quartz&#xff0c;delay queue&#xf…

时间轮-Java实现篇

在前面的文章《时间轮-理论篇》讲了时间轮的一些理论知识&#xff0c;然后根据理论知识。我们自己来实现一个简单的时间轮。 1. 理论抽象 将时间轮的理论进行抽象&#xff0c;主要有两个方面&#xff1a; 时间轮的转动每一个时间间隔任务的处理&#xff0c;从时间间隔的Buke…

时间轮timewheel算法

时间轮是个不太常见&#xff0c;但在部分场景有较高使用价值的工具。 时间轮常用于延时任务&#xff0c;在Netty、akka、Quartz、Zookeeper等高性能组件中都存在时间轮定时器的踪影。 从定时任务说起 自然界中定时任务无处不在&#xff0c;太阳每天东升西落&#xff0c;候鸟的…

定时器实现原理——时间轮

时间轮 时间轮算法是通过一个时间轮去维护定时任务&#xff0c;按照一定的时间单位对时间轮进行划分刻度。然后根据任务延时计算任务落在该时间轮的第几个刻度上&#xff0c;如果任务时长超出了刻度数量&#xff0c;则需要增加一个参数记录时间轮需要转动的圈数。 简单时间轮…

时间轮算法

一、时间轮算法 1. 时间轮的基本概念 Kafka中存在大量的延时操作&#xff0c;如延时生产、延时消费等&#xff1b;而JDK中自带的 Timer 和 DelayQueue 的插入和删除操作的平均复杂度为 O&#xff08;nlogn&#xff09;&#xff0c;无法满足 Kafka 的高性能要求&#xff0c;因…

时间轮算法(TimingWheel)

时间轮算法的应用非常广泛&#xff0c;在 Dubbo、Netty、Kafka、ZooKeeper、Quartz 的组件中都有时间轮思想的应用&#xff0c;甚至在 Linux 内核中都有用到。 其思想理论基础可以参考论文&#xff1a;http://www.cs.columbia.edu/~nahum/w6998/papers/ton97-timing-wheels.pd…

那些惊艳的算法们(三)—— 时间轮

从定时任务说起 自然界中定时任务无处不在&#xff0c;太阳每天东升西落&#xff0c;候鸟的迁徙&#xff0c;树木的年轮&#xff0c;人们每天按时上班&#xff0c;每个月按时发工资、交房租&#xff0c;四季轮换&#xff0c;潮涨潮落&#xff0c;等等&#xff0c;从某种意义上…

1、时间轮

一、什么是时间轮&#xff1f; 作为一个粗人&#xff0c;咱不扯什么高级的词汇&#xff0c;直接上图&#xff1a; 上面是一张时间轮的示意图&#xff0c;可以看到&#xff0c;这个时间轮就像一个钟表一样&#xff0c;它有刻度&#xff0c;图中画了9个格子&#xff0c;每个格子…