前因
由于netty动辄管理100w+的连接,每一个连接都会有很多超时任务。比如发送超时、心跳检测间隔等,如果每一个定时任务都启动一个Timer
,不仅低效,而且会消耗大量的资源。
解决方案
根据George Varghese 和 Tony Lauck 1996 年的论文:Hashed and Hierarchical Timing Wheels: data structures to efficiently implement a timer facility。提出了一种定时轮的方式来管理和维护大量的Timer
调度.
原理
时间轮其实就是一种环形的数据结构,可以想象成时钟,分成很多格子,一个格子代码一段时间(这个时间越短,Timer
的精度越高)。并用一个链表报错在该格子上的到期任务,同时一个指针随着时间一格一格转动,并执行相应格子中的到期任务。任务通过取摸
决定放入那个格子。如下图所示:
以上图为例,假设一个格子是1秒,则整个wheel能表示的时间段为8s,假如当前指针指向2,此时需要调度一个3s后执行的任务,显然应该加入到(2+3=5)的方格中,指针再走3次就可以执行了;如果任务要在10s后执行,应该等指针走完一个round零2格再执行,因此应放入4,同时将round(1)保存到任务中。检查到期任务时应当只执行round为0的,格子上其他任务的round应减1。
是不是很像java中的Hashmap
。其实就是HashMap
的哈希拉链算法,只不过多了指针转动与一些定时处理的逻辑。所以其相关的操作和HashMap
也一致:
- 添加任务:O(1)
- 删除/取消任务:O(1)
- 过期/执行任务:最差情况为O(n)->也就是当
HashMap
里面的元素全部hash冲突,退化为一条链表的情况。平均O(1)->显然,格子越多,每个格子上的链表就越短,这里需要权衡时间与空间。
多层时间轮
如果任务的时间跨度很大,数量很大,单层的时间轮会造成任务的round
很大,单个格子的链表很长。这时候可以将时间轮分层,类似于时钟的时分秒3层。如下图所示:
但是个人认为,多层的时间轮造成的算法复杂度的进一步提升。单层时间轮只需增加每一轮的格子就能解决链表过长的问题。因此,更倾向使用单层的时间轮,netty4中时间轮的实现也是单层的。
netty时间轮的实现-HashedWheelTimer
简单使用示例
1.引入netty依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.4.Final</version>
</dependency>
2.示例代码
示例1:
|
|
输出为:
start:2016-11-30 05:56:35
task :2016-11-30 05:56:38
示例2:
|
|
输出:
start:2016-12-01 08:32:37
task1:2016-12-01 08:32:43
task2:2016-12-01 08:32:43
可以看到,当前一个任务执行时间过长的时候,会影响后续任务的到期执行时间的。也就是说其中的任务是串行执行的。所以,要求里面的任务都要短平快。
HashedWheelTimer源码之构造函数
|
|
再来看下createWheel
的代码:
|
|
normalizeTicksPerWheel()
的代码:
|
|
这里其实不建议使用这种方式,因为当ticksPerWheel的值很大的时候,这个方法会循环很多次,方法执行时间不稳定,效率也不够。推荐使用java8 HashMap的做法:
|
|
HashedWheelTimer源码之启动、停止与添加任务
start()
启动时间轮的方法:
|
|
AtomicIntegerFieldUpdater是JUC里面的类,原理是利用反射进行原子操作。有比AtomicInteger更好的性能和更低得内存占用。跟踪这个类的github 提交记录,可以看到更详细的原因
stop()
停止时间轮的方法:
|
|
newTimeout()
添加定时任务:
|
|
这里使用的Queue不是普通java自带的Queue的实现,而是使用JCTool–一个高性能的的并发Queue实现包。
HashedWheelTimer源码之HashedWheelTimeout
HashedWheelTimeout
是一个定时任务的内部包装类,双向链表结构。会保存定时任务到期执行的任务、deadline、round等信息。
|
|
HashedWheelTimer源码之HashedWheelBucket
HashedWheelBucket
用来存放HashedWheelTimeout,结构类似于LinkedList。提供了expireTimeouts(long deadline)
方法来过期并执行格子中的定时任务
|
|
HashedWheelTimer源码之Worker
Worker
是时间轮的核心线程类。tick的转动,过期任务的处理都是在这个线程中处理的。
|
|
总结
通过阅读源码,学到了很多之前不知道的知识点和注意事项。比如:
- 操作数字型要考虑溢出问题
- System.nanoTime()返回值
- Atomic*FieldUpdater类的运用
- 一些代码设计方式
- 不断优化性能,Lock Less代替Lock;Lock Free代替Lock Less
- JCTool高性能队列的使用