前言

在看这篇文章的时候对其中超时控制一块儿有点好奇。通过时间轮来控制超时?啥是时间轮?怎么控制的?文章会先介绍常见的计时超时处理,再引入时间轮介绍及 netty 在实现时的一些细节,最后总结下实现的一些优缺点。个人观点,如有错误望指正。

计时/超时

JDK 中有许多经典的计时/超时计算的实现。例如 AQS 中的 doAcquireNanosFutureTask 中的 awitDone, 从原理上来讲都是通过以下这种两种计时方式来实现的。

这里有两个问题:

  1. 为什么要用 LockSupport.parkNanos 不用 Object.wait/Thread.sleep?

    LockSupport 是使用来实现底层操作,比如 park/unpark,源码文档开头第一句是 LockSupport 是创建锁或其他同步类的基本线程同步原语(p.s. 个人觉得就是基本工具的意思吧)。根据文档内容我整理了几个要点:

    有三种情况会从休眠中唤醒:

    回到问题,为什么不使用 Object.wait ?个人认为是因为我们不能保证 wait 是在 notify/notifyAll 之前执行的。如果在之后,就会一直阻塞下去。

    为什么不用 Thread.sleep?个人认为首先需要要处理 InterruptedException,其次 sleep 必须休眠设定的时间,无法中途唤醒。

  • 使用 LockSupport 需要每个线程关联一个 permit,类似于 Semaphore 信号量同步类计数的原理,只是不会像 Semaphore 一样累加,类似于只有 0/1 两个值。调用 park 阻塞 permit 为 0,调用 unpark 恢复 permit 为 1。

  • 由于 permit 的原因,线程之间的竞争具有活性(liveness),非 0 即 1,不会产生死锁。

  • no reason return:只要 unpark 就会在任何时候恢复,所以一般建议在循环中使用,时刻检查循环条件,所以 park 其实是自旋的一种优化,避免长时间空转。

  1. unpark 调用
  2. 其他线程调用了休眠线程的 interrupt,但不会抛出 InterruptedException
  3. 虚调用(这个不太了解。。)

为什么要用 System.nanoTime 不用 System.currentTimeMillis?

currentTimeMillis 返回的是当前时间和 1970.01.01 midnight 之间的差值。如果发生时钟回拨或者手动把时钟改到以前,两次记录的时间差值就有可能为负了。

nanoTime 在 JDK 文档中是建议用来做耗时计算的。nanoTime 并不是严格意义上的时间,只是 JVM 实例启动后随机选取的一个固定且任意的原点时间(可能是未来时间,值有可能为负数)开始计时。所以正确使用 nanoTime 的姿势是:

// 耗时计算long startTime = System.nanoTime();long estimatedTime = System.nanoTime() - startTime;

// 比较两个时间long t0 = System.nanoTime();long t1 = System.nanoTime();// 由于存在溢出的问题// t0 // 所以应该使用 t1 - t0 

时间轮算法

image.png

超时的本质个人理解也的确是处理未来到达的定时任务,通过上述的方式可以控制超时需要每个线程独自控制,时间轮的这种方式更适合异步批量。Netty 针对 I/O 超时控制做了一些优化,参考这篇论文实现了 HashedWheelTimer。从上图可知,时间轮会分为固定长度的 bucket,任务根据设定的 delay 时间计算放入指定的 bucket, 同一个 buket 下通过双向链表相连。其实就是一个 HashMap 。HashedWheelTimer 会通过一个线程循环的查每个 bucket 下有哪些已经可以执行的定时任务并执行。从上面的图也可以发现,不同 delay 的定时任务也可能会落到同一个 bucket 下,但并不代表触发时间是相同的,比如上图中有 10 个 tick,定时 1s 和 11s 都会落在 tick 1 上,但定时 11s 应该在下一轮时才触发。所以应该还要记录每个任务需要在第几轮触发。

使用案例

基于 Netty 的中间件有很多,大多都会用到这个 Timer 来做些事情。下面的案例源码来自蚂蚁开源的 sofa-bolt。

  1. 一次正常的异步请求超时控制

    sofa-bolt 中的自行封装的异步请求是与 JDK 中行为一致的。调用后立即返回,通过 future.get()/get(long timeout, TimeUnit unit),获取调用结果。

    invokeWithFuture
  2. 心跳检测超时控制

    sofa-bolt 的实现借用了 Netty 的 IdleStateEvent 触发, 逻辑很简单,就是通过特殊的心跳命令定时去检查连接是否还在线,记录心跳失败的次数,超过设定阈值就抛出异常。所以分为以下两步:

    源码挺好看懂,详细了解可以::point_right: 点这里

    1. 构建一个定时任务触发超时的逻辑

      new-timeout
    2. 根据 response 处理心跳

      处理心跳会在连接上添加一个 Listener,当收到响应时触发。

      invokeCallbackListener

实现细节

image.png

还是需要看下这个图,从图中可以大致看到构造一个时间轮需要的属性。

  1. wheelSize:一个时间轮需要设置多少个 Tick, 默认是 512 个,size 默认会向上取值到最接近的 2 次幂,毕竟位运算计算下标时有奇效。
  2. tickDuration: 每一个 tick 时长的设置。默认是 100 ms。这里如果设置太长可能会积压很多任务在一个 tick 上。

官方建议:不要创建太多 HashedWheelTimer 的实例。时间轮应该是共享的,而不是频繁的创建。并且 HashedWheelTimer 在初始化的时候都会创建一个 worker 线程进行调度,频繁创建也会造成很大的消耗。

所以可以看到在 sofa-bolt 中获取实例是通过单例来处理的。

属性

HashedwheelTimer 使用无锁编程的风格来实现了时间轮算法。所以大量使用了 JUC 下的工具类,是学习并发编程的模版案例了。

HashedWheelBucket & HashedWheelTimeout

  1. HashedWheelBucket 用于存储每个 tick 上的超时任务,是个链表结构,有记录头尾节点,通过 bucket 来完成 timeout 的增删改。节点当然就是 HashedWheelTimeout ,HashedWheelTimeout 记录着前后节点,所以就形成了双端队列。

    HashedWheelBucket HashedWheelTimeout
  2. 链表还支持支持从中间删除,原因是 HashedWheelTimeout 是中有记录自己是在那个 bucket 里,删除的时候使用自己所属的 bucket 来删除自己(我删我自己?)

  3. Bucket 删除定时任务的逻辑,就是简单的链表删除是不是略显枯燥?

尽早 Return

image.png

如果能看懂这个图的话,就不用往下看了,尽早return,下面都是字看着累。彩色的线是 worker 在运行状态下会循环做的几个操作。

  1. 删除存在 cancelledTimeouts 队列中的失效任务
  2. 将缓存在 timeouts 队列中的新任务存放到时间轮上
  3. 执行该段时间内需要触发的定时任务

构造器

构造就是对上述的未赋值的属性做一些补充。节约篇幅展示一些细节:

  1. 构造的时候需要根据设置的 ticksPerWheel 创建对应的一个时间轮 Bucket 数组。用于在每个 tick 中存放对应的超时任务,并且还会把 ticksPerWheel 匹配到最近的 2 次幂上,方便位运算计算下标。

    normalizeBucket
  2. 实例限制,最大 64 个时间轮实例

    instance-limit

创建超时任务

从上述 sofa-bolt 的使用案例中可以看出,一切都是从 newTimeout 开始的,然后就结束了(开箱即用.jpg)...事实上这也就是最核心的逻辑了。看源码的话,省略掉一些参数校验的代码,就剩就十几行。一眼看下去应该也就会好奇start() 里面的逻辑。

newTimeout

start 方法的逻辑也简单,就是启动 woker 线程。有个需要注意的点是,start方法会使用 startTimeInitialized(countDownLatch) 阻塞等待 startTime 赋值完成,毕竟 startTime 是后续超时比较的依据。

woerkStart

Worker

woker 线程是 HashedWheelTimer 的核心,实现了 Runnable 接口。我们通过 newTimeout 创建的定时任务,并不会直接放到时间轮上,而是缓存起来,当 woker 跑起来之后遍历到哪个 tick 就会把缓存队列里对应的这个 tick 下的定时任务放到 bucket 里,然后执行该 tick 下允许触发定时的 timeout。woker 线程在启动后只要没有被关闭就会不停的扫描下去。

worker-run
waitForNextTick

image.png
向 Bucket 添加定时任务

这里会把缓存在 MpscQueue 中的 Timeout 转移到 bucket 中,计算出正确的 bucketIndex 以及对应的轮数。

执行超时任务

触发超时的逻辑很简单,整个流程是标准的双向链表增删改。需要注意的是,文章开头所说的不同定时任务可能活落到同一个 bucket 上,此时需要根据 remainingRrounds 判断是否在当前 tick 下执行。

expireTimeouts

总结

HashedWheelTimer 加上注释只有 800+ 行代码,代码通俗易懂且精巧,非常值得借鉴学习。文章只介绍了新建定时任务的流程,其实还有取消,终止等等的处理,都是值得一看的。当然还是有些不足的地方:

  1. HashedWheelTimer 适合于短平快的业务,由于 worker 是单线程的,耗时过长的定时任务会导致后续的任务阻塞。所以可以看到使用案例基本上都是立即返回的。批量耗时的任务还是应该使用业界流行的定时任务框架。
  2. 内存占用会比较大。前文有说过在创建定时任务的时候并不会直接放到 bucket 里而是先放到一个 MpscQueue 里,当 worker 走到定时任务所在的 tick 时才会将其添加进去。而且 bucket 本身数组 + 链表也会有很大的内存占用。
  3. 如果任务时间跨度过大,remainingRounds 会特别大,如果期间没有其他的定时任务就会空转很长时间,浪费资源。对此 Kafka 有给出优化的方案:层级时间轮,根据时分秒都设置一个时间轮,粒度细分也更好控制。

参考

  1. 定时器的几种实现方式
  2. sofa-bolt 介绍

全文完


以下文章您可能也会感兴趣:

  • 锁优化的简单思路

  • iOS开发:Archive、ipa 和 App 包瘦身

  • 压力测试必知必会

  • 分布式 Session 之 Spring Session 架构与设计

  • 缓存的那些事

  • Java 并发编程 -- 线程池源码实战

  • Lombok Builder 构建器做了哪些事情?

  • WePY 2.0 新特性

  • SSL证书的自动化管理

  • 你真的懂 Builder 设计模式吗?论如何实现真正安全的 Builder 模式

我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。

心跳超时时间设置_定时器实现之时间轮算法相关推荐

  1. 免打扰时间设置_我的视频设置是什么样的,为什么要打扰我

    免打扰时间设置 Rewind half a year: Before a typical in-real-life event, I would take a shower in the mornin ...

  2. java手表怎么设置时间设置时间设置_佳明手表怎么设置时间?

    随着近期人们对健身的热衷,许多腕表厂商纷纷开发起了运动腕表,其中就有一个名为佳明的手表品牌开始为人们所了解,那么你知道佳明手表怎么设置时间吗?下面就由小编来为大家科普一下吧! 佳明手表怎么设置时间?想 ...

  3. 百度时间显示_文章的发布时间对百度优化网站重要吗

    文章的发布时间对百度优化网站重要吗?这个问题,相信很多初做网站优化的萌新朋友都会问到,以小匠个人的经历来分享这个问题的经验,小匠认为,文章的发布时间对优化网站是非常重要的,下面小匠将从实际经历来给大家 ...

  4. 路由器php系统时间设置时间设置时间设置时间设置时间设置,win7电脑提示系统时间设置有误请更新系统日期...

    环境:win7 打开电脑,登陆电脑桌面,我们在电脑桌面右下角会看到时间和日期显示 我们用鼠标点击下方的时间,这时会显示一个当前的日期时间表.鼠标点击下方的"更改日期和时间设置" 这 ...

  5. mysql查询今日没有时间字段_关于日期及时间字段的查询

    前言: 在项目开发中,一些业务表字段经常使用日期和时间类型,而且后续还会牵涉到这类字段的查询.关于日期及时间的查询等各类需求也很多,本篇文章简单讲讲日期及时间字段的规范化查询方法. 1.日期和时间类型 ...

  6. java 定时器时间设置_如何在Java中设置定时器?

    所以答案的第一部分是如何做主题要求的事情,因为这是我最初对它的解释,有几个人似乎觉得很有帮助.这个问题后来被澄清了,我已经扩展了答案来解决这个问题. 设置计时器 首先,您需要创建一个计时器(我使用的是 ...

  7. mysql 设置电脑时间设置_怎样设置mysql密码

    有时我们会因为设置原因或时间长了忘记了数据库管理员的密码,使得我们被关在MySQL服务器外.MySQL服务器提供了一种方法可使我们在服务器上重设密码.在windows和linux/unix平台上操作稍 ...

  8. win7锁屏时间怎么设置_电脑锁屏时间怎么设置

    以WIN10系统为例演示. 1/3 打开"控制面板":点击"电源选项" 2/3 点击"更改计划设置":设置锁屏时间 3/3 点击" ...

  9. js php 获取时间倒计时_,JS实现获取时间和设置倒计时代码分享

    本文主要和大家分享JS实现获取时间和设置倒计时代码,希望能帮助到大家. 只做笔记记录一下,主要用到Date 和 setInterval 第一个倒计时的设置: var timeBox = documen ...

最新文章

  1. [转]WinXP、Win7脚本自动加域及用户资料迁移
  2. USB、TTL电平、232电平之间的相互转换
  3. Java眼中的XML--文件读取--2 应用SAX方式解析XML
  4. boost::shared_lock相关的测试程序
  5. glide缩略图存储 android,Glide 显示视频缩略图及遇到的坑
  6. wordpress linux 目录,快速搭建WordPress(Linux)
  7. 经典面试题(5):小心javascript自动插入分号机制
  8. 计算机d盘不显示容量,电脑D盘可用空间小,可是看不到文件
  9. linux软件中心无法安eclipse,Ubuntu软件中心安装Eclipse无法启动的问题
  10. 局域网电脑访问IIS
  11. Window下完全卸载MySQL教程
  12. Linux自学之旅-安装篇(磁盘分区)
  13. Cdn英文的读音音标_宋sir的美式音标教程 Unit 1 /i/ tea
  14. iPhone7 plus分辨率行不行
  15. 莆系如何投放快手广告?
  16. 智能合约审计之DDOS概述
  17. 小鼠大脑解剖图分区_科学家们绘制小鼠大脑的详细3D结构图谱
  18. springboot2.0设置session失效时间需要使用Duration字符串
  19. 写给自己,人生路远,勿忘初心
  20. 8750H带的动MATLAB2019吗,比7820HK略弱 8代酷睿i7-8750H处理器对比7代跑分测试

热门文章

  1. 更换百度地图图标html,百度地图接口,自定义图标,点击切换图标
  2. SpringBoot 项目构建 Docker 镜像调优实践
  3. MySQL中视图和触发器学习
  4. python爬虫避免重复数据_No.2﹣Python﹣scan﹣anti-crawler(随机请求头和IP代理)取消链接和重复数据消除,NO2pythonscrapy,反,爬虫,去...
  5. leetcode题解46-全排列
  6. (4.32)自定义函数整理大全
  7. UE4 无法include “filename.generated.h”
  8. linux rdate
  9. 让nginx 支持 pathinfo ,支持thinkphp
  10. 创建程序集时元数据失败 -- 拒绝访问_Veeam 云原生数据管理解决方案 Kasten K10 介绍...