工作中研究过Java技术栈下几种常见的定时器实现,在此做个总结:
在游戏服务器中几乎不会使用它,因为有明显的缺点:
首先,它是单线程执行的,如果某个任务执行太长,可能会影响后续任务的准时执行。
其次,游戏中经常有修改系统时间测活动的需求,这种场景不适合。因为它底层是在schedule task的时候就算好了wait的时间,无法在系统时间修改后自动调整。
最主流的实现方式,也是Netty等框架实现定时任务的方式。
基本思路是:开单独线程不断循环,每次sleep固定时间片,然后依次遍历任务队列中的任务,若到时间了就扔到业务线程池执行。任务维护在PriorityQueue的队列中,这种数据结构好处是插入的时间复杂度仅为O(logN),好于链表的O(N)。
因为不存在长时间阻塞,所以可以做到系统时间修改后任务时间自动调整。
适合于时间精度要求不高的场合,比如IO超时处理。
精度不高的原因是时间精度依赖于轮中的格子数,但如果格子数越多,空间消耗也会越大。本质上这是一个时间-空间trade off的问题。
时间轮在Netty、Kafka、Zookeeper等很多Java开源项目中都有广泛应用。仿照实现一个并不难。
因为需要一格格计算,所以无法做到系统时间修改后任务时间自动调整。
这种方式底层实现也是让线程sleep或wait,类似于方式1和2。
不过quartz本身提供了更高阶的功能:如crontab表达式,可以指定日期(如每日零点)定时执行;也能支持更复杂的触发逻辑,如延迟3秒后每1秒执行一次;还提供任务持久化功能,即使宕机后任务也能恢复。
quartz本质上是封装好的更重量级的任务调度方案,类似的还有xxl-job,后者还提供分布式和可视化监控的功能。不过对于游戏来说,quartz一般也够用了。
quartz可以做到系统时间修改后任务时间自动调整。通过quartz源码可以看到,调度线程每次wait不是直接到下个任务执行时间,而是30秒左右的随机值,这样反复wait直到任务执行的时间点。因此修改系统时间后,调度线程在当前wait时间结束后,能将任务执行时间重新调整。
long now = System.currentTimeMillis();
// 默认30秒加随机浮动
long waitTime = now + getRandomizedIdleWaitTime();
long timeUntilContinue = waitTime - now;
synchronized(sigLock) {
try {
if(!halted.get()) {
// QTZ-336 A job might have been completed in the mean time and we might have
// missed the scheduled changed signal by not waiting for the notify() yet
// Check that before waiting for too long in case this very job needs to be
// scheduled very soon
if (!isScheduleChanged()) {
// 在主循环中反复执行wait,直到任务执行
sigLock.wait(timeUntilContinue);
}
}
} catch (InterruptedException ignore) {
}
}
要注意的是,往后调系统时间没有问题,但如果是往回调时间,那么在调整前已过期的任务不会重新执行。例如,用crontab设置了每日0点执行,如果从某天1点调整到前一天23点,那么在经过0点时任务不会重新执行。