Kafka、Dubbo、ZooKeeper、Netty、Caffeine、Akka 中都有对时间轮的实现. 时间轮简单来说就是一个环形队列(底层一般基于数组实现),队列中的每一个元素(时间格)都可以存放一个定时任务列表;时间轮中的每个时间格代表了时间轮的基本时间跨度或者说时间精度,假如时间一秒走一个时间格的话,那么这个时间轮的最高精度就是 1 秒(也就是说 3 s 和 3.9s 会在同一个时间格中)。
下图是一个有 12 个时间格的时间轮,转完一圈需要 12 s。当我们需要新建一个 3s 后执行的定时任务,只需要将定时任务放在下标为 3 的时间格中即可。当我们需要新建一个 9s 后执行的定时任务,只需要将定时任务放在下标为 9 的时间格中即可。
那当我们需要创建一个 13s 后执行的定时任务怎么办呢?这个时候可以引入一叫做 圈数/轮数 的概念,也就是说这个任务还是放在下标为 1 的时间格中, 不过它的圈数为 2。
除了增加圈数这种方法之外,还有一种 多层次时间轮 (类似手表),Kafka 采用的就是这种方案.
时间轮比较适合任务数量比较多的定时任务场景,它的任务写入和执行的时间复杂度都是O(1),时间轮算法具有以下优点: 1.高效性:时间轮算法能够快速地查找和执行任务,特别适合用于大量定时任务的情况; 2.公平性:时间轮算法对所有定时任务都是公平的,每个定时任务都有机会在时间轮上获得一个槽位
相比于延迟队列
延迟队列是基于优先队列实现(PriorityQueue);优先队列采用堆排序,头是按照指定排序方式的最小元素。
结构
优先队列是允许至少下列两种操作的数据结构:insert(插入)、deleteMin(删除最小者),在Java 中体现为 offer 方法和 poll 方法,它们等价于入队、出队操作。
二叉堆(binary heap) 一般借助二叉堆实现优先队列,它有两个性质:结构性、堆序性,一般使用数组实现即可:
堆是一棵完全二叉树,对数组中任一位置i上的元素,其左子节点在位置 2i 上,右子节点在 (2i + 1) 上,它的父节点在 ⌊i / 2⌋ 上。
基本操作
insert(插入)—— percolate up(上滤)
为将一个元素插入堆中,在下一个可用位置处创建一个空穴。若X放入不破坏堆序,则完成插入;否则,我们将空穴的父节点元素移入该空穴,原父节点成为新的空穴,重复此过程,直至满足堆序性质即可,这种策略称为 上滤
deleteMin(删除最小元)—— percolate down(下滤)
删除13之后,试图再次正确地将末置位31放入堆中,除非堆内所有元素相等,否则必定因破坏堆序性质而无法放入。可将较小子元素14置入空穴,空穴下滑一层,重复该过程,直至寻找到合适的空穴,这种策略称为 下滤
解析
Java 中内置了优先队列的实现 PriorityQueue 类(线程不安全)
优先队列底层基于该数组实现(二叉堆载体)
* Priority queue represented as a balanced binary heap: the two
* children of queue[n] are queue[2*n+1] and queue[2*(n+1)]. The
* priority queue is ordered by comparator, or by the elements'
* natural ordering, if comparator is null: For each node n in the
* heap and each descendant d of n, n <= d. The element with the
* lowest value is in queue[0], assuming the queue is nonempty.
*/
/** 注释翻译
* 优先队列 基于 平衡二叉堆,queue[n]的两个孩子为queue[2*n+1]、queue[2*(n+1)]。
* 优先队列根据Comparator接口的实现排序,或根据元素的自然顺序
* 若 comparator 属性为空,对于堆中每个节点n 都有其每个后裔节点d(n <= d)。
* 若队列非空,最小的元素保存在queue[0]
*/
transient Object[] queue; // non-private to simplify nested class access
优点
插入删除的操作复杂度都是O(Logn),无需频繁移动元素,相比于普通数组的复杂度O(n)十分高效