未完待续
引入:
- 详解了时间轮的用法: https://www.zhihu.com/question/361018720/answer/937510941
- 时间轮的来由【10w定时任务,如何高效触发超时】: https://blog.csdn.net/qijiqiguai/article/details/78606877
1. 为啥需要它?
需求:最近要搞IM,里面有个需求,15秒不发消息,判断用户下线。
经过探讨总共得到两种方法:
- 对个每个用户开启一个定时器,15秒后启动它,并判断用户是否下线。
很显然,这不现实,每个定时器都需要一个线程
- 将每个用户最后的请求时间戳缓存下来,然后开启一个定时器,轮询每个用户,判断其是否超时。
仅启动一个定时器,非常nice,但是问题是需要轮询每个用户,也许应该用多个定时器来处理,但是这都无法改变一个问题,需要轮询大量无关用户,有没有办法解决这个问题?
这个两个方法引出了核心需求: 我希望定时器每次启动都只对需要操作的对象操作。即对可能已经失效的用户来操作。
这个需求的核心问题在于按照时间对任务排序。
那为什么要用所谓的时间轮结构?普通队列它不香吗?或者说HashedWheelTimer和java.util.Timer的差别在哪里?
1. 从性能上考虑,(主要体现在插入任务的过程)
:
明显HashedWheelTimer占优。
- HashedWheelTimer插入是直接插入(简称:直插)。
- java.util.Timer的插入是上锁,寻找插入点、插入,解锁。
2. 从执行任务的精准性来讲
:
_ java.util.Timer明显要更准_
- java.util.Timer 直接sleep到指定时间唤醒
- HashedWheelTimer 需要一格一格的启动停止(导致延后一格,宁晚不要早)
3. 从额外损耗cpu来讲
:
_ java.util.Timer更有优势_
- sleep到需要的启动的时刻启动。
- HashedWheelTimer每一个格子都要执行一次。
综合考虑:
- 大并发量的任务,且对时间要求不敏感(晚100ms执行也无所谓的任务)。更好的选择是采用HashedWheelTimer
- 少量任务,更好的选择是java.util.Timer
2. HashedWheelTimer 图解实现步骤
思路:
- 作为一个定时器,我只需要关注它的线程模型(怎么去执行task),怎么添加task,怎么取消task即可。
- 我不可能一辈子都记住源码的细节,所以只能抓住它的精髓,它最本质的模型,
- 记忆最好的方式是去记住它的模型,用图来画出模型来,才是真正理解记忆。
整体图:
2.1 如何实现一个所谓的时间轮
1) 啥叫时间轮呀?
所谓时间轮,就是按照一个固定的时间刻度,周而复始的移动指向刻度的指向,可以理解为时钟的秒针。
2) 如何周而复始?
正常js程序员首先反应的通过 % 来实现,但是作为大佬,用正常人一眼看出来的东西,太丢人了,
所以大佬选中用 & 来实现(ps:大佬就是大佬,为了一丢丢性能,搞出几十行代码)
3) 如何&来实现呀?
%的实现本质上是基于10进制,与一个数与N的余数,
而&想得到余数,可以让这个数&(2^N - 1) 即可。
4) 这样装逼会有啥缺点?
缺点很明显,想要通过&(2^N - 1) 让时间轮上每个刻度周而复始,
就要求时间轮刻度的总数必须是2^N。
这告诉我们,装逼是要付出代价的,但大佬装逼不用
2.2 为啥Task不直接怼进时间轮,而是通过一个队列怼进去呢?
我想了很久,陡然想起,transferTimeoutsToBuckets里面那个没有用常量的100000。 我猜测大佬的想法可能是基于两点:
- 时间轮执行顺序
- 代码在线程安全上结构清晰
1)时间轮执行顺序如何体现?
插入Task的时候,将Task插入到正在运行的Bucket后,Bucket正好跳过去了。
这意味着这个Task可能要等一轮的Buckets时间过去才能执行。
2) 代码在线程安全上结构清晰
这是我对这个整个操作最大的认可
唯一的并发点就在存取队列那块,其他的都是单线程跑,代码结构非常清晰。
我觉得这种结构非常值得我们学习,通过队列来有效的拆分复杂的多线程,
使其并发安全集中于某个小点,而不是整个代码每行都需要防止线程安全问题
3. HashedWheelTimer之迷惑行为
3.1 HashedWheelTimer#waitForNextTick 迷之sleep
很久之前看源码是这么写的:
为什么要这么写呢?
原因是sleep在小于10ms的时候,系统中断太高了。
所以作者直接让sleep小于10ms时,sleep(0),这样线程实际上没有休眠,和Thread.yield类似,也就没有中断了。
然而最近的更新
原因是:当sleep(0)时,主机会疯狂执行waitForNextTick(),导致cpu 100%
对此我真的醉了,这一段逻辑压根就没有意义了,应该从根本上解决问题,限制无限出现10ms的情况。
来源:oschina
链接:https://my.oschina.net/lianghao0/blog/4289332