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

大家好,我是boo!

今天学习Rocket的延迟队列时,看到了一个叫时间轮的概念,接下来我们好好了解一下时间轮究竟是什么东西。

时间轮是一种可以相对精确控制延迟时间精度的数据结构。Kafka和Netty的延迟操作就是基于时间轮实现的。

其实对于延迟操作,Java已经提供了Timer, DelayQueue 配合线程池或者 ScheduledThreadPool。那么为什么还要使用时间轮呢?现在先让我们来了解一下Timer,DelayQueue和ScheduledThreadPool。

Timer

实现

Timer可以实现延迟任务和周期性任务。

从代码中可以看到Timer的核心就是一个优先队列和封装的执行任务的单线程

Timer维护了一个基于时间的小根堆,将最快需要执行的任务放在顶上。根据堆的特性,插入和删除时间都是O(nlogn),TimerThread不断拿堆顶的任务和当前时间对比。若时间已到,执行任务并弹出堆;时间没到则调用wait()等待。

TaskQueue(就是堆的正常操作)

TaskThread的run操作

小结

  1. 基于小根堆实现,在数据量大时,插入和删除的代价大。
  2. 单线程执行,当前任务执行过久可能会影响下一个任务的准时执行。
  3. 没有对异常进行处理,一个任务报错可能导致后面任务都不可用。

ScheduledThreadPoolExecutor

实现

先来看看Timer的注释。

Java 5.0 introduced ScheduledThreadPoolExecutor,It is effectively a more versatile

replacement for the Timer,it allows multiple service threads.Configuring with one

thread makes it equivalent to Timer.

简单来说,ScheduledThreadPoolExecutor只是一个功能更丰富的Timer,同时新增了多服务线程,如果只设置一个服务线程,跟Timer没有太大的区别。

继承了ThreadPoolExecutor,定性操作就是正常的线程池。

小结

  1. 同样是维护优先队列,但执行任务从Timer的单线程变成了多线程,避免了单线程阻塞的问题。
  2. Java线程池的设定是task出错会把错误无声无息的吃了,不会影响后面的任务。

DelayQueue

底层同样是使用优先队列。

总而言之,Timer,ScheduledThreadPool和DelayQueue都是基于优先队列实现的,无法适应Kafka这种海量数据场景的性能要求。而时间轮的插入和删除都是O(1)的。

时间轮

实现

时间轮的类似钟表一样,通过一个环形数组实现,数组每个元素成为槽,槽内部的任务通过双向链表连接,链表插入和删除都是O(1)。

槽位本身也代表了时间精度,假设一秒扫一个槽,那么最高时间精度就是1秒。

表中的时间一共有8个槽0~7,假设槽的时间单位为1秒,此时来了个延迟5秒的任务,那么就进入 5 % 8 + 1 = 6,即第6个槽(下标为5)中,拼接到槽位上双向链表任务的尾部。

每秒扫到下一格,遍历这格的双向链表任务执行。

但是如果插入一个50秒的任务呢,槽位不够怎么办?

有两种实现。

1)增加轮次的概念

50 % 8 + 1 = 3, (50 - 1) / 8 = 6,即第6轮的第3个槽位(下标为2)。简单来说,就是扫了6轮之后,再扫到这个槽时执行。Netty的HashedWheelTimer 使用的就是这种方式。

2)类似时钟的多层次时间轮

和时钟更像,引入类似分针、时间的概念。

假设第一层走一格1秒,那么第二层走一格就花8秒,第三层....

下图的三层,每层8槽,一共24个槽就可以处理512秒的延迟任务。

多层次时间轮还有个降级的操作,假设本来一个500秒的任务本来在第3层,时间过了436秒后,此时还有64秒就会触发任务了,此时就将这个任务降到第2层,再过56秒,还有8秒就执行任务了,此时降到第1层。

降级是为了保证时间精度一致性。Kafka内部用的就是多层次的时间轮算法。

总结

对比而言时间轮更适合任务数很大的延时场景,它的任务插入和删除时间复杂度都为O(1)。对于延迟超过时间轮所能表示的范围有两种处理方式,一是通过增加一个字段-轮数,Netty 就是这样实现的。二是多层次时间轮,Kakfa 是这样实现的。

相比而言 Netty 的实现会有空推进的问题,而 Kafka 采用 DelayQueue 以槽为单位,利用空间换时间的思想解决了空推进的问题。

可以看出延迟任务的实现都不是很精确的,并且或多或少都会有阻塞的情况,即使你异步执行,线程不够的情况下还是会阻塞。

参考文章

本篇基本的内容和截图基本都出自下面这篇文章,其中也包含着Kafka和Netty中具体实现源码的分析,相当精彩!!

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值