面试官:你给我说一下什么是时间轮吧?

面试时遇到『看门狗』脖子上挂着『时间轮』,我就问你怕不怕? Redisson的看门狗和Netty的时间轮,了解一下?写的过程中顺便打了一下自己的脸。技术嘛,不就是在不断打脸的过程中成长起来的嘛。 荒腔走板聊生活 大家好,一周的时间过的飞快,转眼间又到周末了。 老规矩,还是本号特色,先是荒腔走板的聊聊生活。 上面的图片是我在一次跑步的过程中拍的,一只狗子。可以看到图片中还有一个轨迹图,也是一只狗子。 这个轨迹图全长21km,刚好是一个半马的距离,而且一路上... 阅读详情

今天我带大家来卷一下时间轮吧,这个玩意其实还是挺实用的。

常见于各种框架之中,偶现于面试环节,理解起来稍微有点难度,但是知道原理之后也就觉得:

大多数人谈到时间轮的时候都会从 netty 开始聊。

我就不一样了,我想从 Dubbo 里面开始讲,毕竟我第一次接触到时间轮其实是在 Dubbo 里面,当时就惊艳到我了。

而且,Dubbo 的时间轮也是从 Netty 的源码里面拿出来的,基本一模一样。

时间轮在 Dubbo 里面有好几次使用,比如心跳包的发送、请求调用超时时间的检测、还有集群容错策略里面。

我就从 Dubbo 里面这个类说起吧:

org.apache.dubbo.rpc.cluster.support.FailbackClusterInvoker

Failback,属于集群容错策略的一种:

你不了解 Dubbo 也没有关系,你只需要知官网上是这样介绍它的就行了:

我想突出的点在于“定时重发”这四个字。

我们先不去看源码,提到定时重发的时候,你想到了什么东西?

是不是想到了定时任务?

那么怎么去实现定时任务呢?

大家一般都能想到 JDK 里面提供的 ScheduledExecutorService 和 Timer 这两个类。

Timer 就不多说了,性能不够高,现在已经不建议使用这个东西。

ScheduledExecutorService 用的还是相对比较多的,它主要有三个类型的方法:

简单说一下 scheduleAtFixedRate 和 scheduleWithFixedDelay 这两个方法。

ScheduleAtFixedRate,是每次执行时间为上一次任务开始起向后推一个时间间隔。

ScheduleWithFixedDelay,是每次执行时间为上一次任务结束起向后推一个时间间隔。

前者强调的是上一个任务的开始时间,后者强调的是上一个任务的结束时间。

你也可以理解为 ScheduleAtFixedRate 是基于固定时间间隔进行任务调度,而 ScheduleWithFixedDelay 取决于每次任务执行的时间长短,是基于不固定时间间隔进行任务调度。

所以,如果是我们要基于 ScheduledExecutorService 来实现前面说的定时重发功能,我觉得是用 ScheduleWithFixedDelay 好一点,含义为前一次重试完成后才应该隔一段时间进行下一次重试。

让整个重试功能串行化起来。

那么 Dubbo 到底是怎么实现这个定时重试的需求的呢?

撸源码啊,源码之下无秘密。

准备发车。

撸源码

有的同学看到这里可能着急了:不是说讲时间轮吗,怎么又开始撸源码了呀?

你别猴急呀,我这不得循序渐进嘛。

我先带你手撕一波 Dubbo 的源码,让你知道源码这样写的问题是啥,然后我再说解决方案嘛。

再说了,我直接,啪的一下,把解决方案扔你脸上,你也接受不了啊。

我喜欢温柔一点的教学方式。

好了,先看下面的源码。

这几行代码你要是没看明白没有关系,你主要关注 catch 里面的逻辑。

我把代码和官网上的介绍帮你对应了一下。

意思就是调用失败了,还有一个 addFailed 来兜底。

addFailed 是干了啥事呢?

干的就是“定时重发”这事:

org.apache.dubbo.rpc.cluster.support.FailbackClusterInvoker#addFailed

这个方法就可以回答前面我们提出的问题:Dubbo 集群容错里面,到底是怎么实现这个定时重试的需求的呢?

从标号为 ① 的地方可以知道,用的就是 ScheduledExecutorService,具体一点就是用的 scheduleWithFixedDelay 方法。

再具体一点就是如果集群容错采用的是 failback 策略,那么在请求调用失败的 RETRY_FAILED_PERIOD 秒之后,以每隔 RETRY_FAILED_PERIOD 秒一次的频率发起重试,直到重试成功。

RETRY_FAILED_PERIOD 是多少呢?

看第 52 行,它是 5 秒。

另外,你可以在前面 addFailed 方法中看到标号为 ③ 的地方,是在往 failed 里面 put 东西。

failed 又是一个什么东西呢?

看前面的 61 行,是一个 ConcurrentHashMap。

标号为 ③ 的地方,往 failed put 的 key 就是这一次需要重试的请求,value 是处理这一次请求对应的服务端。

failed 这个 map 是什么时候用呢?

请看标号为 ② 的 retryFailed 方法:

在这个方法里面会去遍历 failed 这个 map,全部拿出来再次调用一遍。

如果成功了就调用 remove 方法移除这个请求,没有成功的会抛出异常,打印日志,然后等待下次再次重试。

到这里我们就算是揭开了 Dubbo 的 FailbackClusterInvoker 类的神秘面纱。

面纱之下,隐藏的就是一个 map 加 ScheduledExecutorService。

感觉好像也没啥难的啊,很常规的解决方案嘛,我也能想到啊。

于是你缓缓的在屏幕上打出一个:

但是,朋友们,抓好坐稳,要“但是”了,要转弯了。

这里面其实是有问题的,最直观的就是这个 map,没有限制大小,由于没有限制大小,那么在一些高并发的场景下,是有可能出现内存溢出的。

好,那么问题来了,怎么防止内存溢出呢?

很简单,首先我们可以限制 map 的大小,对吧。

比如限制它的容量为 1000。

满了之后,怎么办呢?

可以搞一个淘汰策略嘛,先进先出(FIFO),或者后进先出(LIFO)。

然后也不能一直重试,如果重试超过了一定次数应该被干掉才对。

上面说的内存溢出和解决方案,都不是我乱说的。

我都是有证据的,因为我是从 FailbackClusterInvoker 这个类的提交记录上看到了它的演进过程的,前面截图的代码也是优化之前版本的代码,并不是最新的代码:

这一次提交,提到了一个编号叫 2425 的 issue。

https://github.com/apache/dubbo/issues/2425

这里面提到的问题和解决方案,就是我前面说的事情。

终于,铺垫完成,关于时间轮的故事要正式开始了。

时间轮原理

有的朋友又开始猴急了。

要我赶紧上时间轮的源码。

你别着急啊,我直接给你讲源码,你肯定会看懵逼的。

所以我决定,先给你画图,看懂原理。

给大家画一下时间轮的基本样子,理解了时间轮的工作原理,下面的源码解析理解起来也就相对轻松一点了。

首先时间轮最基本的结构其实就是一个数组,比如下面这个长度为 8 的数组:

怎么变成一个轮呢?

首尾相接就可以了:

假如每个元素代表一秒钟,那么这个数组一圈能表达的时间就是 8 秒,就是这样的:

注意我前面强调的是一圈,为 8 秒。

那么 2 圈就是 16 秒, 3 圈就是 24 秒,100 圈就是 800 秒。

这个能理解吧?

我再给你配个图:

虽然数组长度只有 8,但是它可以在上叠加一圈又一圈,那么能表示的数据就多了。

比如我把上面的图的前三圈改成这样画:

希望你能看明白,看不明白也没有关系,我主要是要你知道这里面有一个“第几圈”的概念。

好了,我现在把前面的这个数组美化一下,从视觉上也把它变成一个轮子。

轮子怎么说?

轮子的英文是 wheel,所以我们现在有了一个叫做 wheel 的数组:

然后,把前面的数据给填进去大概是长这样的。

为了方便示意,我只填了下标为 0 和 3 的位置,其他地方也是一个意思:

那么问题就来了。假设这个时候我有一个需要在 800 秒之后执行的任务,应该是怎么样的呢?

800 mod 8 =0,说明应该挂在下标为 0 的地方:

假设又来一个 400 秒之后需要执行的任务呢?

同样的道理,继续往后追加即可:

不要误以为下标对应的链表中的圈数必须按照从小到大的顺序来,这个是没有必要的。

好,现在又来一个 403 秒后需要执行的任务,应该挂在哪儿?

403 mod 8 = 3,那么就是这样的:

我为什么要不厌其烦的给你说怎么计算,怎么挂到对应的下标中去呢?

因为我还需要引出一个东西:待分配任务的队列。

上面画 800 秒、 400 秒和 403 秒的任务的时候,我还省略了一步。

其实应该是这样的:

任务并不是实时挂到时间轮上去的,而是先放到一个待分配的队列中,等到特定的时间再把待分配队列中的任务挂到时间轮上去。

具体是什么时候呢?

下面讲源码的时候再说。

其实除了待分配队列外,还有一个任务取消的队列。

因为放入到时间轮的任务是可以被取消的。

比如在 Dubbo 里面,检验调用是否超时也用的是时间轮机制。

假设一个调用的超时时间是 5s,5s 之后需要触发任务,抛出超时异常。

但是如果请求在 2s 的时候就收到了响应,没有超时,那么这个任务是需要被取消的。

对应的源码就是这块,看不明白没关系,看一眼就行了,我只是为了证明我没有骗你:

org.apache.dubbo.remoting.exchange.support.DefaultFuture#received

原理画图出来大概就是这样,然后我还差一张图。

把源码里面的字段的名称给你对应到上面的图中去。

主要把这几个对象给你对应上,后面看源码就不会太吃力了:

对应起来是这样的:

注意左上角的“worker的工作范围”把整个时间轮包裹了起来,后面看源码的时候你会发现其实整个时间轮的核心逻辑里面没有线程安全的问题,因为 worker 这个单线程把所有的活都干完了。

最后,再提一嘴:比如在前面 FailbackClusterInvoker 的场景下,时间轮触发了重试的任务,但是还是失败了,怎么办呢?

很简单,再次把任务放进去就行了,所以你看源码里面,有一个叫做 rePut 的方法,干的就是这事:

org.apache.dubbo.rpc.cluster.support.FailbackClusterInvoker.RetryTimerTask#run

这里的含义就是如果重试出现异常,且没有超过指定重试次数,那么就可以再次把任务仍回到时间轮里面。

等等,我这里知道“重试次数”之后,还能干什么事儿呢?

比如如果你对接过微信支付,它的回调通知有这样的一个时间间隔:

我知道当前重试的次数,那么我就可以在第 5 次重试的时候把时间设置为 10 分钟,扔到时间轮里面去。

时间轮就可以实现上面的需求。

当然了,MQ 的延迟队列也可以,但是不是本文的讨论范围。

但是用时间轮来做上面这个需求还有一个问题:那就是任务在内存中,如果服务挂了就没有了,这是一个需要注意的地方。

除了 FailbackClusterInvoker 外,其实我觉得时间轮更合适的地方是做心跳。

这可太合适了, Dubbo 的心跳就是用的时间轮来做。

org.apache.dubbo.remoting.exchange.support.header.HeartbeatTimerTask#doTask

从上图可以看到,doTask 方法就是发送心跳包,每次发送完成之后调用 reput 方法,然后再次把发送心跳包的任务仍回给时间轮。

好了,不再扩展应用场景了。

接下来,进入源码分析,跟上节奏,不要乱,大家都能学。

开卷!

时间轮源码

前面把原理理解到位了,接下来就可以看一下我们的源码了。

先说明一下,为了方便我截图,下面的部分截图我是移动了源码的位置,所以可能和你看源码的时候有点不一样。

我们再次审视 Dubbo 的 FailbackClusterInvoker 类中关于时间轮的用法。

首先 failTimer 这个对象,是一个很眼熟的双重检查的单例模式:

这里初始化的 failTimer 就是 HashedWheelTimer 对象关键的逻辑是调用了它的构造方法。

所以,我们先从它的构造方法入手,开始撕它。

先说一下它的几个入参分别是干啥的:

  • threadFactory:线程工厂,可以指定线程的名称和是否是守护进程。

  • tickDuration:两个 tick 之间的时间间隔。

  • unit:tickDuration 的时间单位。

  • ticksPerWheel:时间轮里面的 tick 的个数。

  • maxPendingTimeouts:时间轮中最大等待任务的个数。

所以,Dubbo 这个时间轮的含义就是这样的:

创建一个线程名称为 failback-cluster-timer 的守护线程,每隔一秒执行一次任务。这个时间轮的大小为 32,最大的等待处理任务个数是 failbackTasks,这个值是可以配置的,默认值是 100。

但是很多其他的使用场景下,比如 Dubbo 检查调用是否超时,就没有送 maxPendingTimeouts 这个值:

org.apache.dubbo.remoting.exchange.support.DefaultFuture#TIME_OUT_TIMER

它甚至连 ticksPerWheel 都没有上送。

其实这两个参数都是有默认值的。ticksPerWheel 默认为 512。maxPendingTimeouts 默认为 -1,含义为对等待处理的任务个数不限制:

好了,现在我们整体看一下这个时间轮的构造方法,每一行的作用我都写上了注释:

有几个地方,我也单独拿出来给你说一下。

比如 createWheel 这个方法,如果你八股文背的熟悉的话,你就知道这里和 HashMap 里面确认容量的核心代码是一样一样的。

这也是我在源码注释里面提到的,时间轮里面数组的大小必须是 2 的 n 次方。

为什么,你问我为什么?

别问,问就是为了后面做位运算,操作骚,速度快,逼格高。

我相信下面的这一个代码片段不需要我来解释了,你要是不理解,就再去翻一番 HashMap 的八股文:

但是这一行代码我还是可以多说一句的 mask = wheel.length - 1。

因为我们已经知道 wheel.length 是 2 的 n 次方。

那么假设我们的定时任务的延迟执行时间是 x,那么它应该在时间轮的哪个格子里面呢?

是不是应该用 x 对长度取余,也就是这样计算: x % wheel.length。

但是,取余操作的效率其实不算高。

那么怎么能让这个操作快起来呢?

就是 wheel.length - 1。

wheel.length 是 2 的 n 次方,减一之后它的二级制的低位全部都是 1,举个例子就是这样式儿的:

所以 x % wheel.length = x & (wheel.length - 1)。

在源码里面 mask =wheel.length - 1。

那么 mask 在哪用的呢?

其中的一个地方就是在 Worker 类的 run 方法里面:

org.apache.dubbo.common.timer.HashedWheelTimer.Worker

这里计算出来的 idx 就是当前需要处理的数组的下标。

我这里只是告诉你 mask 确实是参与了 & 位运算,所以你看不懂这块的代码也没有关系,因为我还没讲到这里来。

所以没跟上的同学不要慌,我们接着往下看。

前面我们已经有一个时间轮了,那么怎么调用这个时间呢?

其实就是调用它的 newTimeout 方法:

这个方法有三个入参:

含义很明确,即指定任务(task)在指定时间(delay,unit)之后开始触发。

接下来解读一下 newTimeout 方法:

里面最关键的代码是 start 方法,我带大家看一下到底是在干啥:

分成上下两部分讲。

上面其实就是维护或者判断当前 HashedWheelTimer 的状态,从源码中我们知道状态有三个取值:

  • 0:初始化

  • 1:已启动

  • 2:已关闭

如果是初始化,那么通过一个 cas 操作,把状态更新为已启动,并执行 workerThread.start() 操作,启动 worker 线程。

下面这个部分就稍微有一点点费解了。

如果 startTime 等于 0,即没有被初始化的话,就调用 CountDownLatch 的 await 等待一下下。

而且这个 await 还是在主线程上的 await,主线程在这里等着 startTime 被初始化,这是个什么逻辑呢?

首先,我们要找一下 startTime 是在哪儿被初始化的。

就是在 Worker 的 run 方法里面,而这个方法就是在前面 workerThread.start() 的时候触发的:

org.apache.dubbo.common.timer.HashedWheelTimer.Worker

可以看到,对 startTime 初始化完成后,还判断了是否等于 0。也就是说 System.nanoTime() 方法是有可能返回为 0,一个小细节,如果你去要深究一下的话,也是很有趣的,我这里就不展开了。

startTime 初始化完成之后,立马执行了 startTimeInitialized.countDown() 操作。

这不就和这里呼应起来了吗?

主线程不马上就可以跑起来了吗?

那么问题就来了,这里大费周章的搞一个 startTime 初始化,搞不到主线程还不能继续往下执行是干啥呢?

当然是有用啦,回到 newTimeout 方法接着往下看:

我们分析一下上面这个等式哈。

首先 System.nanoTime() 是代码执行到这个地方的实时时间。

因为 delay 是一个固定值,所以 unit.toNanos(delay) 也是一个固定值。

那么 System.nanoTime()+unit.toNanos(delay) 就是这个任务需要被触发的纳秒数。

举个例子。

假设 System.nanoTime() = 1000,unit.toNanos(delay)=100。

那么这个任务被触发的时间点就是 1000+100=1100。

这个能跟上吧?

那么为什么要减去 startTime 呢?

startTime 我们前面分析了,其实初始化的时候也是 System.nanoTime(),初始化完成后就是一个固定值了。

那岂不是 System.nanoTime()-startTime 几乎趋近于 0?

这个等式 System.nanoTime()+unit.toNanos(delay)-startTime 的意义是什么呢?

是的,这就是我当时看源码的一个疑问。

但是后面我分析出来,其实整个等式里面只有 System.nanoTime() 是一个变量。

第一次计算的时候 System.nanoTime()-startTime 确实趋近于 0,但是当第二次触发的时候,即第二个任务来的时候,计算它的 deadline 的时候,System.nanoTime() 可是远大于 startTime 这个固定值的。

所以,第二次任务的执行时间点应该是当前时间加上指定的延迟时间减去 worker 线程的启动时间,后面的时间以此类推。

前面 newTimeout 方法就分析完了,也就是主线程在这个地方就执行完时间轮相关的逻辑了。

接下来该分析什么呢?

肯定是该轮到时间轮的 worker 线程上场发挥了啊。

worker 线程的逻辑都在 run 方法里面。

而核心逻辑就在一个 do-while 里面:

循环结束的条件是当前时间轮的状态不是启动状态。

也就是说,只要时间轮没有被调用 stop 逻辑,这个线程会一直在运行。

接下来我们逐行看一下循环里面的逻辑,这部分逻辑就是时间轮的核心逻辑。

首先是 final long deadline = waitForNextTick() 这一行,里面就很有故事:

首先你看这个方法名你就知道它是干啥的了。

是在这里面等待,直到下一个时刻的到来。

所以方法进来第一行就是计算下一个时刻的纳秒值是啥。

接着看 for 循环里面,前面部分都看的比较懵逼,只有标号为 ③ 的地方好理解的多,就是让当前线程睡眠指定时间。

所以前面的部分就是在算这个指定时间是什么。

怎么算的呢?

标号为 ① 的地方,前面部分还能看懂,

deadline - currentTime 算出来的就是还需要多长时间才会到下一个时间刻度。

后面直接就看不懂了。

里面的 1000000 好理解,单位是纳秒,换算一下就是 1 毫秒。

这个 999999 是啥玩意?

其实这里的 999999 是为了让算出来的值多 1 毫秒。

比如,deadline - currentTime 算出来是 1000123 纳秒,那么 1000123/1000000=1ms。

但是(1000123+999999)/1000000=2ms。

也就是说要让下面标号为 ③ 的地方,多睡 1ms。

这是为什么呢?

我也不知道,所以我先暂时不管了,留个坑嘛,问题不大,接着往下写。

下面就到了标号为 ② 的地方,看起来是对 windows 操作系统进行了特殊的处理,要把 sleepTimeMs 换算为 10 的倍数。

为啥?

这里我就得批评一下 Dubbo 了,把 Netty 的实现拿过来了,还把关键信息给隐藏了,这不合适吧。

这地方在 Netty 的源码中是这样的:

这里很清晰的指了个路:

https://github.com/netty/netty/issues/356

而顺着这条路,一路往下跟,会找到这样一个地方:

https://www.javamex.com/tutorials/threads/sleep_issues.shtml

没想到还有意外收获。

第一个划线的地方大概意思是说当线程调用 Thread.sleep 方法的时候,JVM 会进行一个特殊的调用,将中断周期设置为 1ms。

因为 Thread.sleep 方法的实现是依托于操作系统提供的中断检查,也就是操作系统会在每一个中断的时候去检查是否有线程需要唤醒并且提供 CPU 资源。所以我觉得前面多睡 1ms 的原因就可以用这个原因来解释了。

前面留的坑,这么快就填上了,舒服。

而第二个划线的地方说的是,如果是 windows 的话,中断周期可能是 10ms 或者 15ms,具体和硬件相关。

所以,如果是 windows 的话,需要把睡眠时间调整为 10 的倍数。

一个没啥卵用的知识,送给你。

前面几个问题了解清楚了,waitForNextTick 方法也就理解到位了,它干的事儿就是等,等一个时间刻度的时间,等一个 tick 长度的时间。

等到了之后呢?

就来到了这一行代码 int idx = (int) (tick & mask)

我们前面分析过,计算当前时间对应的下标,位运算,操作骚,速度快,逼格高,不多说。

然后代码执行到这个方法 processCancelledTasks()

看方法名称就知道了,是处理被取消的任务的队列:

逻辑很简单,一目了然,就是把 cancelledTimeouts 队列给清空。

这里是在 remove,在清理。

那么哪里在 add,在添加呢?

就是在下面这个方法中:

org.apache.dubbo.common.timer.HashedWheelTimer.HashedWheelTimeout#cancel

如果调用了 HashedWheelTimeout 的 cancel 方法,那么这个任务就算是被取消了。

前面画图的时候就提到了这个方法,逻辑也很清晰,所以不多解释了。

但是你注意我画了下划线的地方:MpscLinkedQueue。

这是个啥?

这是一个非常牛逼的无锁队列。

但是 Dubbo 这里的 cancelledTimeouts 队列的数据结构明明用的是 LinkedBlockingQueue 呀?

怎么回事呢?

因为这里的注释是 Netty 里面的,Netty 里面用的是 MpscLinkedQueue。

你看我给你对比一下 Netty 和 Dubbo 这里的区别:

所以这里的注解是有误导的,你有时间的话可以给 Dubbo 提给 pr 修改一下。

又拿捏了一个小细节。

好了,我们接着往下卷,来到了这行代码 HashedWheelBucket bucket=wheel[idx]

一目了然,没啥说的。

从时间轮里面获取指定下标的 bucket。

主要看看它下面的这一行代码 transferTimeoutsToBuckets()

我还是每一行都加上注释:

所以这个方法的核心逻辑就是把等待分配的任务都发配到指定的 bucket 上去。

这里也就回答了我画图的时候留下的一个问题:什么时候把等待分配队列里面的任务挂到时间轮上去呢?

就是这个时候。

接下来分析 bucket.expireTimeouts(deadline) 这一行代码。

你看这个方法的调用方就是 bucket,它代表的含义就是准备开始处理这个 bucket 里面的这个链表中的任务了:

最后,还有一行代码 tick++

表示当前这个 tick 已经处理完成了,开始准备下一个时间刻度。

关键代码就分析完了。

一遍看不懂就多看一遍,但是我建议你自己也对照着源码一起看,很快就能搞懂。

相信以后面试官问到时间轮的时候你可以和他战斗上一个回合了。

为什么是一个回合呢?

因为得你回答完这个时间轮后,一般来说,面试官会追问一个:

嗯,说的很不错,那你再介绍一下层级时间轮吧?

当时你就懵逼了:什么,层级时间轮是什么鬼,歪歪没写啊?

是的,怪我,我没写,下次,下次一定。

但是我可以给你指条路,去看看 kafka 对于时间轮的优化。你会看的鼓起掌来。

几个相关的 issues

最后,关于 Dubbo 时间轮,在 issues 里面有一个讨论:

https://github.com/apache/dubbo/issues/3324

大家有兴趣的可以去看看。

其中提到了一个有意思的问题:

Netty 在 3.x 中有大量使用 HashedWheelTimer,但是在 4.1 中,我们可以发现,Netty 保留了 HashedWheelTimer,但在其源码中并未使用它,而是选择了 ScheduledThreadPoolExecutor,不知道它的用意是什么。

这个问题得到了 Netty 的维护者的亲自答:

https://github.com/netty/netty/issues/8774

他的意思是时间轮其实没有任何毛病,我没有用只是因为我们希望与通道的EventLoop位于同一线程上。

在 Netty 里面,有个老哥发现时间轮并没有用上了,甚至想把它给干掉:

我寻思这属于工具类啊,你留着呗,总是会有用的。

另外,前面的 issue 还提到了另外一个问题:

https://github.com/apache/dubbo/issues/1371

这也是 Dubbo 引入时间轮之后进行的优化。

带你看一眼,上面是优化之后的,下面是之前的写法:

在之前的写法中,就是后台起一个线程,然后搞个死循环,一遍遍的去扫整个集合:

这种方案也能实现需求,但是和时间轮的写法比起来,高下立判。

操作骚,速度快,逼格高。

最后说一句

好了,看到了这里了,转发、在看、点赞随便安排一个吧,要是你都安排上我也不介意。写文章很累的,需要一点正反馈。

给各位读者朋友们磕一个了:

LVGL在STM32H743IIT6上的性能优化技巧:如何提升图形界面刷新速度 本文详细介绍了在STM32H743IIT6高性能MCU上优化LVGL图形界面刷新速度的实战技巧。通过利用芯片的DMA2D硬件加速模块替代低效的软件画点,将绘制缓冲区放入SDRAM并启用双缓冲机制,以及合理配置CPU缓存与MPU,有效解决了界面卡顿问题。文章还涵盖了LVGL引擎调优、内存架构优化等关键步骤,帮助开发者充分发挥STM32H743的硬件潜力,实现流畅的图形界面体验。 阅读详情

相关推荐

联想Win10改Win7设置BIOS指南

通过本文的指南,你应该能够成功地在联想Win10电脑上设置BIOS以支持Windows 7的安装和运行。尽管Windows 10提供了许多现代化的功能和改进,但如果你因特定需求而更倾向于使用Windows 7,那么本文提供的步骤将帮助你顺利实现这一转换。请记住,在更改操作系统前务必做好充分的准备工作,并在安装过程中保持耐心和细心。祝你安装顺利!

mmoo_python的博客 2035

时间轮(TimingWheel)

而过期的时间格可以继续复用。理解了时间格的复用之后,再看回刚刚的例子,当前时间是2s时,添加延时时间为19s的任务,那么这个任务就会插入到过期时间为21s的时间格中。比如当前时间是0s,我们添加了个延时时间为2s的任务,那么这个任务的过期时间就是2s,也就是当前时间0s再走两秒,变成了2s的时候,就到了触发这个定时任务的时间。如果这个时候又插入一个延时时间为8s的任务进来, 这个任务的过期时间就是在当前时间2s的基础上加8s, 也就是10s, 那么这个任务将会插入到过期时间为10s的时间格中。

提供各种学习资源及帮助 4743

PDPS16.0单机版与SPLMLicenseServer授权避坑指南:从安装到冲突解决

本文详细解析了PDPS16.0单机版的安装与授权流程,重点阐述了如何正确配置SPLMLicenseServer本地许可证服务器以实现软件激活。针对安装过程中常见的许可证冲突问题,提供了清晰的排查思路和解决方案,并介绍了通过设置环境变量等替代授权方法,帮助用户高效避坑,确保Tecnomatix Process Simulate顺利运行。

weixin_29231221的博客 559

时间轮(TimingWheel)算法总结

通过阅读篇文章您可以很容易理解平时所使用的开源框架是如何进行任务调度的。而且对于以后业务上碰到需要做时间任务调度的需求,也可以尝试着用实践论算法去实现。一、时间轮的应用其实早在1987年,...

ixxqq的博客 1747

别再为环保数采仪通讯发愁!手把手教你用MCGS触摸屏+4G模块发送HJ212报文

本文详细介绍了如何使用MCGS触摸屏配合4G模块高效发送HJ212标准报文,解决工业环保监测中的数据采集与传输难题。从硬件连接到软件配置,再到报文组装与发送策略,提供全流程实用指南,特别针对4G网络环境下的特殊设置给出优化建议,帮助工程师提升数据传输稳定性与效率。

weixin_30847865的博客 388

双足步行机器人的ZMP与CoP检测

静态步行与动态步行   机器人步态分为静态步行和动态步行。当机器人做静态步行运动时,身体的各个部分运动速度很小,机器人的整体稳定性较易控制。静态步行稳定性采用机器人的重心地面投影点(Center of Gravity,简称CoG)作为稳定性标准,这种判定方法适用于运动较为缓慢的情况。如果机器人采用的是一种高速运动的步行方式,则称之为动态步行。当机器人进行动态步行时,其重心的位置和加速度的大小时...

weixin_34112208的博客 5906

时间轮 (史上最全)

缓存之王 Caffeine 中,涉及到100w级、1000W级、甚至亿级元素的过期问题,如何进行高性能的定时调度,是一个难题。

架构师尼恩 3676

DSP28335与Vofa+上位机通信实战:SCIb配置避坑指南(附完整代码)

本文提供DSP28335与Vofa+上位机通信的实战指南,重点解析SCIb模块配置的常见问题与解决方案。针对研旭开发板的硬件连接、波特率精确计算、FIFO高效使用以及FireWater协议封装进行了详细说明,并附有完整代码,帮助开发者快速实现实时数据可视化与波形分析,提升调试效率。

weixin_29172963的博客 149

时间轮算法(TimingWheel)

时间轮算法的应用非常广泛,在 Dubbo、Netty、Kafka、ZooKeeper、Quartz 的组件中都有时间轮思想的应用,甚至在 Linux 内核中都有用到。

Ulrich蚊子Blog 8446

2.4G跳频算法 2.4G切信道算法 (简例示意nRF52832)

2.4G跳频算法 nRF52832示例 2.4G自适应信道算法

Peter.Min 954

《大数据: Hive 交互命令和常用配置》

一、Hive 交互命令 1. 首先解决一个可能发生的问题 (log4 jar包冲突) 我这里解决方法就是删除一个,我删的是hive下的包 yexiang@hadoop1:<hive>$ hive SLF4J: Class path contains multiple SLF4J bindings. SLF4J: Found binding in [jar:file:/opt/soft/apache-hive-2.3.7-bin/lib/log4j-slf4j-impl-2.6.2.ja.

yexiang优快云的专栏 287

定时任务之时间轮算法

如上图中所示,我只需要把任务放到它需要被执行的时刻,然后等着时针转到这个时刻时,取出该时刻放置的任务,执行就可以了。这就是时间轮算法最核心的思想了。首先,时间轮的刻度可以用数组或者链表表示,每个刻度就是一个槽,槽用来存放该刻度需要执行的任务,如果有多个任务需要执行呢?1.时间刻度太多会导致时间轮走到的多数刻度没有任务执行,比如一个月就2个任务,按照一个月30天算,需要移动720次,其中718次是无用的。同理,当月轮移动到刻度12(12号)时,取出这个刻度下的任务,丢到天轮上,天轮接管该任务,到9点执行。

夜尽天明 1319

一篇文章带你快速入门 MATLAB

文章目录一、认识 MATLAB 界面二、MATLAB 的运行方式三、Matlab 中的窗口1. 命令窗口2. MATLAB的程序编辑窗口3. 工作空间窗口4. 命令历史窗口5. 图形窗口6. 当前目录窗口四、MATLAB的帮助系统1. 帮助命令2. 联机帮助五、其他知识补充1. MATLAB的搜索路径2. 变量命名规则3. M文件命名规则 一、认识 MATLAB 界面 在 MATLAB Desktop 中,工具条取代了菜单和工具栏。应用程序库收纳了 MATLAB 产品系列的应用程序,单一文件应用程序打包为一

南淮北安的博客 5125

面试官:说说延迟任务的时间轮调度算法?

面试题大全:www.javacn.site本文继续讨论 Netty 相关的面试题,今天咱们来看一道 Netty 中的高频面试题:说说 Netty 延迟任务的时间轮调度算法?Netty 框架是以性能著称的框架,因此在它的框架中使用了大量提升性能的机制,例如 Netty 用于实现延迟队列的时间轮调度算法就是一个典型的例子。使用时间轮算法可以实现海量任务新增和取消任务的时间度为 O(1),那么什么是时间...

磊哥聊Java 926

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

面试官:知道时间轮算法吗?在Netty和Kafka中如何应用的?

最近看 Kafka 看到了时间轮算法,记得以前看 Netty 也看到过这玩意,没太过关注。今天就来看看时间轮到底是什么东西。为什么要用时间轮算法来实现延迟操作?延时操作 Java 不是提...

HollisChuang's Blog 1332

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

面试Java后端却问我时间轮算法,面试官没想到我看过Dubbo源码!

在ZooKeeper、Netty、Quartz、Kafka 以及 Linux 都需要定时任务的管理功能。 JDK 提供的 java.util.Timer 和 DelayedQueue 等工具类,可以帮助我们实现简单的定时任务管理,其底层实现使用的是堆这种数据结构,存取操作的复杂度都是 O(nlog(n)),无法支持大量的定时任务。在定时任务量比较大、性能要求比较高的场景中,为了将定时任务的存取操作以及取消操作的时间复杂度降为 O(1),一般会使用时间轮的方式。 时间轮是一种高效的、批量管理定时任务的调度模型

JavaEdge全是干货的技术号 1896

面试官:知道时间轮算法吗?在Netty和Kafka中如何应用的?为什么不用Timer、延时线程池?

大家好,我是yes。 最近看 Kafka 看到了时间轮算法,记得以前看 Netty 也看到过这玩意,没太过关注。今天就来看看时间轮到底是什么东西。 为什么要用时间轮算法来实现延迟操作? 延时操作 Java 不是提供了 Timer 么? 还有 DelayQueue 配合线程池或者 ScheduledThreadPool 不香吗? 我们先来简单看看 Timer、DelayQueue 和 ScheduledThreadPool 的相关实现,看看它们是如何实现延时任务的,源码之下无秘密。再来剖析下为何 Netty

yes 889

priorityqueue 的 add和offer方法有区别吗_面试官:时间轮算法知道吗?在Kafka中怎么用的?...

最近看 Kafka 看到了时间轮算法,记得以前看 Netty 也看到过这玩意,没太过关注。今天就来看看时间轮到底是什么东西。为什么要用时间轮算法来实现延迟操作?延时操作 Java 不是提供了 Timer 么?还有 DelayQueue 配合线程池或者 ScheduledThreadPool 不香吗?我们先来简单看看 Timer、DelayQueue 和 ScheduledThreadPo...

weixin_39956451的博客 1661

时间轮的介绍(对比Java原生的Timer,ScheduledThreadPool和DelayQueue)

对比而言时间轮更适合任务数很大的延时场景,它的任务插入和删除时间复杂度都为O(1)。对于延迟超过时间轮所能表示的范围有两种处理方式,一是通过增加一个字段-轮数,Netty 就是这样实现的。二是多层次时间轮,Kakfa 是这样实现的。相比而言 Netty 的实现会有空推进的问题,而 Kafka 采用 DelayQueue 以槽为单位,利用空间换时间的思想解决了空推进的问题。可以看出延迟任务的实现都不是很精确的,并且或多或少都会有阻塞的情况,即使你异步执行,线程不够的情况下还是会阻塞。

weixin_42617227的博客 809

你了解时间轮(Time Wheel)吗?有哪些应用场景?

任务链表 | 同一槽位的任务按链表存储,执行时遍历处理。| 环形数组 | 存储时间槽,每个槽对应一个时间间隔(如1秒)。| 指针(tick) | 周期性移动(如每秒走一格),触发当前槽的任务。第二层(分钟级):60槽,每槽1分钟,覆盖1~59分钟的任务。第三层(小时级):24槽,每槽1小时,覆盖1~23小时的任务。好的,这个问题我需要结合时间轮的设计原理和实际应用来回答。第一层(秒级):60槽,每槽1秒,覆盖0~59秒的任务。追问1:时间轮如何处理超出当前时间轮范围的任务?

2302_81141145的博客 926
上一篇: 大数据——配置并启动集群/开启历史服务器和日志聚集
下一篇: CPU被挖矿,Redis竟是内鬼。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值