首页 > 图灵资讯 > java面试题>正文

javaNetty面试题-HashedTimerWheel

2024-05-23 13:23:20

生成月统计报表、每日得分结算、邮件定时推送

定时任务三种形式:

1、按固定周期定时执行

2、延迟一定时间后执行

3、指定某个时刻执行

定时任务的三个关键方法:

Schedule 新增任务至任务集合;

Cancel 取消某个任务;

Run 执行到期的任务

JDK自带的三种定时器:Timer、DelayedQueue 和 ScheduledThreadPoolExecutor

Timer小根堆队列,deadline 任务位于堆顶端,弹出的始终是最优先被执行的任务。Run 操作时间复杂度 O(1),Schedule 和Cancel 操作的时间复杂度都是 O(logn)。

不论有多少任务被加入数组,始终由 异步线程TimerThread 负责处理。TimerThread 会定时轮询 TaskQueue 中的任务,如果堆顶的任务的 deadline 已到,那么执行任务;如果是周期性任务,执行完成后重新计算下一次任务的 deadline,并再次放入小根堆;如果是单次执行的任务,执行结束后会从 TaskQueue 中删除。

DelayedQueue 采用优先级队列 PriorityQueue延迟获取对象的阻塞队列。DelayQueue中的每个对象都必须实现Delayed 接口,并重写 compareTo 和 getDelay 方法。

DelayQueue 提供了 put() 和 take() 的阻塞方法,可以向队列中添加对象和取出对象。对象被添加到 DelayQueue 后,会根据 compareTo() 方法进行优先级排序。getDelay() 方法用于计算消息延迟的剩余时间,只有 getDelay <=0 时,该对象才能从 DelayQueue 中取出。

DelayQueue 在日常开发中最常用的场景就是实现重试机制。例如,接口调用失败或者请求超时后,可以将当前请求对象放入 DelayQueue,通过一个异步线程 take() 取出对象然后继续进行重试。如果还是请求失败,继续放回 DelayQueue。可以设置重试的最大次数以及采用指数退避算法设置对象的 deadline,如 2s、4s、8s、16s ……以此类推。DelayQueue的时间复杂度和Timer基本一致。

为了解决 Timer 的设计缺陷,JDK 提供了功能更加丰富的 ScheduledThreadPoolExecutor,多线程、相对时间、对异常Timer 的任务调度是基于系统绝对时间的,如果系统时间不正确,可能会出现问题。

TimerTask 如果执行出现异常,Timer 并不会捕获,会导致线程终止,其他任务永远不会执行。

时间轮原理分析

根据任务的到期时间进行取余和取模,然后根据取余结果将任务分布到不同的 slot 中,每个slot中根据round值决定是否操作,每次轮询到指定slot时,总时遍历最少round的对象进行执行,这样新增、执行两个操作的时间复杂度都近似O(1)。如果冲突较大可以增加数组长度,或者采用多级时间轮的方式处理。

public HashedWheelTimer( ThreadFactory threadFactory, //线程池,但是只创建了一个线程 long tickDuration, //时针每次 tick 的时间,相当于时针间隔多久走到下一个 slot TimeUnit unit, //表示 tickDuration 的时间单位,tickDuration * unit int ticksPerWheel, //时间轮上一共有多少个 slot,默认 512 个。boolean leakDetection, long maxPendingTimeouts) {//最大允许等待任务数 // 省略其他代码 wheel = createWheel(ticksPerWheel); // 创建时间轮的环形数组结构 mask = wheel.length - 1; // 用于快速取模的掩码 long duration = unit.toNanos(tickDuration); // 转换成纳秒处理 workerThread = threadFactory.newThread(worker); // 创建工作线程 leak = leakDetection || !workerThread.isDaemon() ? leakDetector.track(this) : null; // 是否开启内存泄漏检测 this.maxPendingTimeouts = maxPendingTimeouts; // 最大允许等待任务数,HashedWheelTimer 中任务超出该阈值时会抛出异常}


时间轮空推进问题

Netty 中的时间轮是通过固定的时间间隔 tickDuration 进行推动的,如果长时间没有到期任务,那么会存在时间轮空推进的现象,从而造成一定的性能损耗。此外,如果任务的到期时间跨度很大,例如 A 任务 1s 后执行,B 任务 6 小时之后执行,也会造成空推进的问题。

Kafka解决方案

为了解决空推进的问题,Kafka 借助 JDK 的 DelayQueue 来负责推进时间轮。DelayQueue 保存了时间轮中的每个 Bucket,并且根据 Bucket 的到期时间进行排序,最近的到期时间被放在 DelayQueue 的队头。Kafka 中会有一个线程来读取 DelayQueue 中的任务列表,如果时间没有到,那么 DelayQueue 会一直处于阻塞状态,从而解决空推进的问题。虽然DelayQueue 插入和删除的性能不是很好,但这其实就是一种权衡的策略,但是DelayQueue 只存放了 Bucket,Bucket 的数量并不多,相比空推进带来的影响是利大于弊的。

为了解决任务时间跨度很大的问题,Kafka 引入了层级时间轮,如下图所示。当任务的 deadline 超出当前所在层的时间轮表示范围时,就会尝试将任务添加到上一层时间轮中,跟钟表的时针、分针、秒针的转动规则是同一个道理。

上一篇 javaNetty面试题-FastThreadLocal
下一篇 javaNetty面试题-select、poll、epoll的区别

文章素材均来源于网络,如有侵权,请联系管理员删除。