设计一个高效的定时任务系统( 三 )


因为有阻塞和异常终止的缺点,JDK 又封装了另一个定时器的实现方式,这次保证不会阻塞 。
因为它是线程池实现方式的一种:ScheduledExecutorService 。ScheduledExecutorService 内部将任务封装之后交给了 DelayQueue 。
DelayQueue 是一个依靠 AQS 队列同步器所实现的无界延迟阻塞队列,内部通过 PriorityQueue 来实现,本质还是还是一个堆,所以插入的时间复杂度也是 O(lgn) 。
③Netty 封装的时间轮:HashedWheelTimer
上面简要描述了操作系统中的时间轮实现,在著名框架 Netty 中也封装了一个自己的时间轮实现:HashedWheelTimer 类 。
因为 Netty 中需要管理大量的 I/O 超时事件,基于时间轮的方案有助于节省资源 。
Netty 中采用一个轮子的方案,一个格子代表的时间是 100ms,默认有 512 个格子 。
来看看 HashedWheelTimer 的构造函数参数:
HashedWheelTimer(ThreadFactory threadFactory, //类似于Clock中的updater, 负责创建Worker线程.long tickDuration,//时间刻度之间的时长(默认100ms), 通俗的说, 就是多久tick++一次.TimeUnit unit,//tickDuration的单位.int ticksPerWheel//类似于Clock中的wheel的长度(默认512). ); 另外为了不无休止的增加 bucket,这里采用了轮(round)的概念,一轮所花费的时间:round time=ticksPerWheel*tickDuration 。
如果 bucket 只有 512 个,而当前休眠时间长于一轮,那么就增加相应的轮次来表示当前休眠时长 。
HashedWheelTimer 中有一些主要的成员:

  • HashedWheelTimer 类本身,主要负责启动 Worker 线程、添加任务等 。
  • Worker:内部负责添加任务,累加 tick,执行任务等 。
  • HashedWheelTimeout:任务的包装类,链表结构,负责保存 deadline,轮数等 。
  • HashedWheelBucket:wheel 数组元素,负责存放 HashedWheelTimeout 链表 。
Worker 线程是 HashedWheelTimer 的核心,主要负责每当已过 tickDuration 时间就累加一次 tick 。
同时也负责执行到期的 timeout 任务和添加 timeout 任务到指定的 wheel 中 。
当添加 Timeout 任务的时候,会根据设置的时间来计算出需要等待的时间长度,根据时间长度进而算出要经过多少次 tick,然后根据 tick 的次数来算出经过多少轮最终得出 task 在 wheel 中的位置 。
对于这种时间轮一般是怎么遍历判断任务到期呢?每个 ticket 到来,都要去遍历每一个 bucket ,以此来判断是否有 bucket 到期 。
所以这种方式就要求 bucket 尽量不要太多,如果太多每次遍历都需要很长的时间 。另外就是每次都会遍历,必然会有很多空转,也是一种资源的浪费 。
④Kafka 中的时间轮:TimingWheel
Netty 中的时间轮实现采用了单轮+round 的模式,在 Kafka 中采用了多轮的模式 。
上面说过多轮模式下如果按照时分秒来表达,每个轮所需的 bucket 都非常的少,遍历轮的时候就会很快 。
但是多轮也会带来另一个问题就是轮的维护:比如有个定时任务是 1*60*60+50=36050s,这时候就需要分钟和秒轮同时维护这个任务 。
当这个任务继续走,只剩下 59s 的时候,分钟轮就无需在维护它的信息,只剩下秒轮来维护,这里出现了降轮的概念。
单机定时机制对比
以上简单描述了各个实现方案,简单对比可以得出:
Timer 的实现方案毋庸置疑是最差的 。阻塞,异常退出这两条“罪名”无疑让现代程序员无法承受因为出错被老板骂的锅 。
ScheduledExecutorService 使用线程池的方式来异步的执行任务,当任务量巨大的时候,如果设置了优先数量的可执行线程,无疑还是会阻塞任务,好在可执行线程多 。
而 HashedWheelTimer 是面向 bucket 设计,如果采用多轮的方式可以不受任务量限制,任务量非常大的时候,维护数组的成本远远要低于维护堆的成本 。
但是如果是任务量很少的情况,时间轮依旧需要全盘扫描,出现空转的状态,这种空载无疑也是浪费资源的提现 。
所以面向使用场景编程的话:
  • 如果当前待运行的定时任务属于耗时长一点,任务量也不是那么大的时候,可以采用 ScheduledExecutorService 的方式来实现 。
  • 如果任务量比较大,任务耗时短,无疑使用 HashedWheelTimer 对内存更加友好 。
定时任务系统
前面从操作系统时钟源开始,说到时钟中断产生了时钟滴答,所有的定时任务都依赖于此 。
软件层面,通过各种有效的算法在节约资源的前提下通过监听时钟滴答来实现任务 。
还记得开篇提到我们本篇文章的意图是什么吗,要设计一个高效的定时任务系统 。


推荐阅读