
文章目录
-
- 一、时间轮算法:原理与设计目标
- [二、核心数据结构:HashedWheelTimer 的构造与初始化](#二、核心数据结构:HashedWheelTimer 的构造与初始化)
-
- [2.1 Timer / TimerTask / Timeout 接口体系](#2.1 Timer / TimerTask / Timeout 接口体系)
- [2.2 HashedWheelTimer 的核心字段](#2.2 HashedWheelTimer 的核心字段)
- [2.3 createWheel() 的初始化细节](#2.3 createWheel() 的初始化细节)
- [2.4 tickDuration 校验](#2.4 tickDuration 校验)
- [2.5 start() 的延迟启动](#2.5 start() 的延迟启动)
- [三、任务提交:newTimeout() 的"先入队"策略](#三、任务提交:newTimeout() 的"先入队"策略)
- [四、Worker 驱动引擎:run() 主循环](#四、Worker 驱动引擎:run() 主循环)
-
- [4.1 run() 的完整源码](#4.1 run() 的完整源码)
- [4.2 主循环的四步衔接](#4.2 主循环的四步衔接)
- [4.3 退出后的清理](#4.3 退出后的清理)
- [五、时间轮精度控制:waitForNextTick() 与转移任务](#五、时间轮精度控制:waitForNextTick() 与转移任务)
-
- [5.1 waitForNextTick():精度、等待与平台兼容](#5.1 waitForNextTick():精度、等待与平台兼容)
- [5.2 transferTimeoutsToBuckets():批量转移的核心逻辑](#5.2 transferTimeoutsToBuckets():批量转移的核心逻辑)
- [六、Bucket 内部:HashedWheelTimeout 的状态管理与过期执行](#六、Bucket 内部:HashedWheelTimeout 的状态管理与过期执行)
-
- [6.1 HashedWheelTimeout 的字段与状态机](#6.1 HashedWheelTimeout 的字段与状态机)
- [6.2 cancel() 的"延迟取消"机制](#6.2 cancel() 的"延迟取消"机制)
- [6.3 expire() 的 CAS 防护与执行路径](#6.3 expire() 的 CAS 防护与执行路径)
- [6.4 HashedWheelBucket 的链表操作](#6.4 HashedWheelBucket 的链表操作)
- [七、停止与回收:stop() 的优雅关闭](#七、停止与回收:stop() 的优雅关闭)
-
- [7.1 时间轮的局限性](#7.1 时间轮的局限性)
- [7.2 HashedWheelTimer 的定位与使用场景](#7.2 HashedWheelTimer 的定位与使用场景)
- 八、整体链路串联
- 全文小结
在网络编程中,定时任务无处不在------连接超时检测、心跳保活、空闲连接回收、重试延迟等场景都需要可靠的定时调度能力。JDK 提供的 ScheduledExecutorService 基于优先队列(DelayedWorkQueue)实现,每次插入和删除都是 O(log n) 复杂度,在海量定时任务场景下性能会急剧退化。Netty 借鉴了 George Varghese 和 Tony Lauck 在 1987 年发表的论文《Hashed and Hierarchical Timing Wheels: Data Structures for the Efficient Implementation of a Timer Facility》中提出的**时间轮(Timing Wheel)**算法,自研了 HashedWheelTimer,用环形数组 + 指针推进替代优先队列,将定时任务的核心操作复杂度降至 O(1)。
时间轮算法如何用环形数组 + tick 指针实现 O(1) 的任务调度?与传统优先队列方案相比,它的核心优势是什么?HashedWheelTimer 的 newTimeout() 提交任务后,任务并非直接入桶,而是先进入 MPSC 队列,批量转移的设计意图是什么?Worker.run() 作为时间轮的驱动引擎,waitForNextTick() → processCancelledTasks() → transferTimeoutsToBuckets() → expireTimeouts() 四步循环的每一步是如何衔接的?HashedWheelTimeout 的 INIT → CANCELLED / EXPIRED 状态机如何保证线程安全?cancel() 为什么是"延迟取消"而非"立即移除"?本文将沿着"任务提交 → 时间轮驱动 → 到期执行 → 优雅关闭"的完整生命周期,逐一分析 HashedWheelTimer 的环形数组存储结构、Worker 主循环的 tick 推进机制、HashedWheelTimeout 的状态管理以及 HashedWheelBucket 的链表操作细节。
一、时间轮算法:原理与设计目标
在深入源码之前,我们首先需要理解时间轮算法的核心思想。所谓时间轮 ,是指将时间轴等分为若干个长度为 tickDuration 的槽位(tick),每个槽位对应一个 bucket,时间轮的指针按固定频率推进,每推进一格就检查当前 bucket 中的任务是否到期。这种设计本质上是一种"空间换时间"的策略------用环形数组的 O(1) 定位能力,替代优先队列的 O(log n) 堆排序开销。
传统 JDK 定时方案中,ScheduledExecutorService 内部使用 DelayedWorkQueue(基于二叉堆的优先队列)来管理定时任务。每次 schedule() 提交任务时,需要将任务插入堆的合适位置,平均复杂度 O(log n);每次从堆顶取出最早到期的任务时,也需要 O(log n) 的堆化操作。在网络 IO 场景下,一个 IdleStateHandler 可能需要同时管理数万个连接的空闲检测,此时优先队列的路由开销会显著放大。
时间轮的解决方案非常直观:将时间切分为固定长度的 tick 片段,每个 tick 对应环形数组中的一个 slot。任务提交时,根据其到期时间计算出目标 tick 位置,将任务放入对应的 bucket 中。Worker 线程每 tick 推进一次指针,检查当前指针对应的 bucket 中是否有到期任务,若有则执行。核心操作(插入、定位、遍历)全部是 O(1) 复杂度。

上面图展示了时间轮的核心结构。环形数组 wheel[8] 有 8 个 bucket,tick 指针按顺时针方向推进。当 tick = 3 时,检查 wheel[3] 中的双向链表,remainingRounds = 0 的任务(T1、T3)在本轮到期执行,remainingRounds > 0 的任务(T2)仅递减剩余轮数。
需要注意的是,HashedWheelTimer 实现的是单层时间轮,而非前述原始论文中提到的三层时间轮(Hierarchical Timing Wheels)。单层时间轮覆盖的时间范围由 wheel.length * tickDuration 决定,当任务延迟超过这个范围时,remainingRounds 字段会记录该任务需要"转多少圈"才到期。这也是 Netty 官方文档中强调"适合近似精度的 IO 超时场景"的原因------默认 tick 间隔 100ms,wheel 大小 512,单轮覆盖约 51.2 秒,对大多数 IO 超时场景已足够。
二、核心数据结构:HashedWheelTimer 的构造与初始化
在了解时间轮算法原理之后,我们来看看 HashedWheelTimer 的接口体系和核心数据结构。HashedWheelTimer 实现了 Timer 接口,其构造过程涉及 wheel 数组的创建、tickDuration 的校验、Worker 线程的初始化等多个关键步骤,整体的构造与启动时序如下:

上面的时序图清晰地展示了两个阶段:构造阶段完成 wheel 数组的创建和 Worker 线程的初始化,首次调用 newTimeout() 时延迟启动 Worker 线程。接下来我们按顺序分析每个细节。
2.1 Timer / TimerTask / Timeout 接口体系
HashedWheelTimer 是一套接口体系的具体实现,我们先看这三个核心接口的定义。
Timer 是定时器的顶层抽象,定义了提交任务和停止定时器两个方法:
java
io.netty.util.Timer
/**
* Schedules {@link TimerTask}s for one-time future execution in a background
* thread.
*/
public interface Timer {
Timeout newTimeout(TimerTask task, long delay, TimeUnit unit);
Set<Timeout> stop();
}
TimerTask 是定时任务的抽象,定义了到期执行和取消回调两个方法。注意 cancelled() 是 default 方法,默认空实现,子类可按需重写:
java
io.netty.util.TimerTask
/**
* A task which is executed after the delay specified with
* {@link Timer#newTimeout(TimerTask, long, TimeUnit)}.
*/
public interface TimerTask {
void run(Timeout timeout) throws Exception;
default void cancelled(Timeout timeout) {
// By default do nothing.
}
}
Timeout 是任务提交后返回的句柄,调用方通过它可以查询任务状态或取消任务:
java
io.netty.util.Timeout
/**
* A handle associated with a {@link TimerTask} that is returned by a
* {@link Timer}.
*/
public interface Timeout {
Timer timer();
TimerTask task();
boolean isExpired();
boolean isCancelled();
boolean cancel();
}
这三个接口的职责划分非常清晰:Timer 负责调度,TimerTask 负责执行逻辑,Timeout 负责生命周期管理。这种"调度---执行---控制"三权分立的接口设计,使得调用方可以灵活地组合使用。
2.2 HashedWheelTimer 的核心字段
HashedWheelTimer 的核心字段可以分为四组:时间轮数组、线程控制、任务队列、执行器。
首先是时间轮数组相关字段:
java
io.netty.util.HashedWheelTimer
// 时间轮环形数组,每个槽位是一个 HashedWheelBucket,内部维护双向链表
private final HashedWheelBucket[] wheel;
// 位掩码,wheel.length - 1,用于 tick & mask 快速定位
private final int mask;
// 每个 tick 的持续时间(纳秒),默认 100ms
private final long tickDuration;
然后是线程控制相关字段:
java
io.netty.util.HashedWheelTimer
// Worker 实例,实现了 Runnable,是驱动时间轮的核心引擎
private final Worker worker = new Worker();
// Worker 线程,由构造器中的 ThreadFactory 创建
private final Thread workerThread;
// Worker 状态常量:0=INIT, 1=STARTED, 2=SHUTDOWN
public static final int WORKER_STATE_INIT = 0;
public static final int WORKER_STATE_STARTED = 1;
public static final int WORKER_STATE_SHUTDOWN = 2;
// Worker 状态字段,通过 AtomicIntegerFieldUpdater 原子更新
private volatile int workerState;
// CountDownLatch,保证 start() 在 Worker 线程初始化 startTime 后才返回
private final CountDownLatch startTimeInitialized = new CountDownLatch(1);
任务队列相关字段:
java
io.netty.util.HashedWheelTimer
// 新提交的任务先入队此 MPSC 队列,Worker 线程在每个 tick 批量转移
private final Queue<HashedWheelTimeout> timeouts = PlatformDependent.newMpscQueue();
// 已取消的任务入队此队列,Worker 线程在每个 tick 开始时批量处理
private final Queue<HashedWheelTimeout> cancelledTimeouts = PlatformDependent.newMpscQueue();
// 当前等待中的任务数量(AtomicLong 保证多线程可见性)
private final AtomicLong pendingTimeouts = new AtomicLong(0);
// 最大等待任务数,默认 -1 不限制,正值时超过上限抛出 RejectedExecutionException
private final long maxPendingTimeouts;
执行器与泄漏检测:
java
io.netty.util.HashedWheelTimer
// 任务执行器,默认 ImmediateExecutor.INSTANCE(直接在 Worker 线程中执行)
private final Executor taskExecutor;
// 资源泄漏追踪器,用于检测 HashedWheelTimer 是否被正确关闭
private final ResourceLeakTracker<HashedWheelTimer> leak;
// 全局实例计数器,超过 64 个实例时打印 ERROR 日志
private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger();
private static final int INSTANCE_COUNT_LIMIT = 64;
从字段设计可以看出 HashedWheelTimer 的三个核心设计决策:第一,timeouts 和 cancelledTimeouts 使用 MPSC(多生产者单消费者)队列,多线程提交任务时无需加锁,Worker 线程消费时无需加锁;第二,workerState 使用 AtomicIntegerFieldUpdater 而非 AtomicInteger,减少了对象封装开销;第三,taskExecutor 默认使用 ImmediateExecutor.INSTANCE,这意味着定时任务到期后直接在 Worker 线程中执行,若任务执行时间过长会阻塞后续 tick 的推进------这是设计权衡,调用方可以通过自定义 Executor 来规避。
将上述四组字段整合起来,HashedWheelTimer 的组件全景如下:

上图将 HashedWheelTimer 的四组核心字段(时间轮数组、线程控制、任务队列、执行器与监控)和内部 Worker 引擎的关系可视化。两组 MPSC 队列是调用方线程与 Worker 线程之间的桥梁:timeouts 负责新任务的提交,cancelledTimeouts 负责取消请求的传递,两端的生产与消费完全解耦。
2.3 createWheel() 的初始化细节
createWheel() 方法负责创建时间轮数组,其核心逻辑是将 ticksPerWheel 归一化为 2 的幂次,然后预分配所有 bucket 对象:
java
io.netty.util.HashedWheelTimer#createWheel
private static HashedWheelBucket[] createWheel(int ticksPerWheel) {
// 归一化为 2 的幂次:如 500 → 512,1000 → 1024
ticksPerWheel = MathUtil.findNextPositivePowerOfTwo(ticksPerWheel);
HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel];
for (int i = 0; i < wheel.length; i ++) {
// 预分配所有 bucket,避免运行时动态创建
wheel[i] = new HashedWheelBucket();
}
return wheel;
}
之所以要在构造阶段就预分配所有 bucket,是因为 HashedWheelTimer 的设计哲学是"用空间换时间"------预先分配 512(默认)个 bucket 对象,运行时直接通过 tick & mask 定位,无需任何动态内存分配。归一到 2 的幂次则是为了用位运算 tick & mask 替代 tick % wheel.length 的取模运算,位运算效率远高于取模。
2.4 tickDuration 校验
HashedWheelTimer 的默认配置为 tickDuration=100ms、ticksPerWheel=512,可通过多个构造器重载按需调整。所有构造器最终收敛到同一个完整构造器,其中 tickDuration 做了两重校验:
java
io.netty.util.HashedWheelTimer#HashedWheelTimer
// 将外部传入的 tickDuration 转换为纳秒
long duration = unit.toNanos(tickDuration);
// 防止溢出:确保 tickDuration(纳秒) * wheel.length 不超过 Long.MAX_VALUE
if (duration >= Long.MAX_VALUE / wheel.length) {
throw new IllegalArgumentException(String.format(
"tickDuration: %d (expected: 0 < tickDuration in nanos < %d",
tickDuration, Long.MAX_VALUE / wheel.length));
}
// 最小精度保护:tickDuration 不能小于 1ms
if (duration < MILLISECOND_NANOS) {
logger.warn("Configured tickDuration {} smaller than {}, using 1ms.",
tickDuration, MILLISECOND_NANOS);
this.tickDuration = MILLISECOND_NANOS;
} else {
this.tickDuration = duration;
}
这里有两个细节:一是溢出校验,因为 waitForNextTick() 中会计算 tickDuration * (tick + 1),如果 tickDuration 太大,乘以 ticksPerWheel 可能溢出;二是最小精度保护,tickDuration 不能小于 1ms,因为 waitForNextTick() 使用 Thread.sleep() 等待,过小的 tickDuration 会导致 CPU 空转。源码注释中也明确说明:"在大多数网络应用中,IO 超时不需要精确到毫秒级"。
2.5 start() 的延迟启动
start() 方法采用 CAS 状态机来控制 Worker 线程的启动,支持幂等调用:
java
io.netty.util.HashedWheelTimer#start
public void start() {
int state = WORKER_STATE_UPDATER.get(this);
switch (state) {
case WORKER_STATE_INIT:
// CAS 原子转换 INIT → STARTED,只有第一个调用者能成功
if (WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_INIT, WORKER_STATE_STARTED)) {
workerThread.start();
}
break;
case WORKER_STATE_STARTED:
// 已启动,直接返回
break;
case WORKER_STATE_SHUTDOWN:
// 已关闭,抛出异常
throw new IllegalStateException("cannot be started once stopped");
default:
throw new Error("Invalid WorkerState: " + state);
}
// 等待 Worker 线程完成 startTime 的初始化
while (startTime == 0) {
try {
startTimeInitialized.await();
} catch (InterruptedException ignore) {
// 忽略中断,startTime 很快就会被初始化
}
}
}
这里的 startTimeInitialized 是一个 CountDownLatch,Worker 线程在 run() 方法中初始化 startTime 后调用 countDown(),从而保证 start() 返回后 startTime 一定可用。newTimeout() 方法中计算 deadline 时需要用到 startTime,这个同步机制确保了后续计算的正确性。
每创建一个 HashedWheelTimer 实例就会启动一个后台线程,这也是官方文档中强调"不要创建太多实例,建议整个应用共享一个"的原因。Netty 内部通过 INSTANCE_COUNTER 全局计数,超过 64 个实例时打印 ERROR 日志,提醒开发者注意资源泄漏。
在进入后续章节的流程分析之前,下面这张表格汇总了 HashedWheelTimer 涉及的全部核心类及其关键方法,方便读者在阅读后续章节时随时回查:
| 类 | 包路径 | 关键方法 |
|---|---|---|
| Timer | io.netty.util | newTimeout(), stop() |
| TimerTask | io.netty.util | run(Timeout), cancelled(Timeout) |
| Timeout | io.netty.util | timer(), task(), isExpired(), isCancelled(), cancel() |
| HashedWheelTimer | io.netty.util | newTimeout(), start(), stop(), pendingTimeouts(), createWheel() |
| HashedWheelTimer.Worker | io.netty.util.HashedWheelTimer | run(), waitForNextTick(), transferTimeoutsToBuckets(), processCancelledTasks(), unprocessedTimeouts() |
| HashedWheelTimer.HashedWheelTimeout | io.netty.util.HashedWheelTimer | expire(), cancel(), remove(), removeAfterCancellation(), compareAndSetState(), state(), isCancelled(), isExpired(), run() |
| HashedWheelTimer.HashedWheelBucket | io.netty.util.HashedWheelTimer | addTimeout(), expireTimeouts(), remove(), clearTimeouts(), pollTimeout() |
三、任务提交:newTimeout() 的"先入队"策略
在 HashedWheelTimer 的构造和启动流程就绪之后,我们来看任务提交流程。newTimeout() 是调用方与 HashedWheelTimer 的交互入口,它的设计决策直接影响调用方线程的性能表现。
java
io.netty.util.HashedWheelTimer#newTimeout
@Override
public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
// 参数校验
checkNotNull(task, "task");
checkNotNull(unit, "unit");
// 原子递增待处理任务计数
long pendingTimeoutsCount = pendingTimeouts.incrementAndGet();
// 背压机制:超过 maxPendingTimeouts 上限时拒绝提交
if (maxPendingTimeouts > 0 && pendingTimeoutsCount > maxPendingTimeouts) {
pendingTimeouts.decrementAndGet();
throw new RejectedExecutionException("Number of pending timeouts ("
+ pendingTimeoutsCount + ") is greater than or equal to maximum allowed pending "
+ "timeouts (" + maxPendingTimeouts + ")");
}
// 确保 Worker 线程已启动(延迟启动,首次调用时触发)
start();
// 计算 deadline:相对于 startTime 的偏移量,而非绝对时间戳
long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;
// 溢出防护:delay > 0 但 deadline < 0 说明发生了数值溢出
if (delay > 0 && deadline < 0) {
deadline = Long.MAX_VALUE;
}
// 创建 HashedWheelTimeout 并加入 MPSC 队列,而非直接插入 bucket
HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
timeouts.add(timeout);
return timeout;
}
这段代码的核心设计思想是"延迟添加"------任务不直接插入 bucket,而是先入队 timeouts(MPSC 队列),由 Worker 线程在每个 tick 的 transferTimeoutsToBuckets() 中批量转移。这个设计有三个关键收益:
第一,调用方线程零阻塞 。timeouts 是 MpscUnboundedArrayQueue(多生产者单消费者无锁队列),调用方线程(可能是多个业务线程)通过 CAS 操作将任务入队,无需任何锁竞争。Worker 线程是唯一的消费者,在每个 tick 中批量 poll 队列并转移任务,入队和出队之间完全解耦。
第二,deadline 计算公式的巧妙之处 。deadline = System.nanoTime() + delay - startTime 计算的是相对于 startTime 的偏移量,而非绝对时间戳。这样做的好处是避免了 startTime 在 newTimeout() 调用时还未初始化的问题------start() 方法通过 startTimeInitialized.await() 保证了 startTime 一定可用。同时,相对偏移量也让 waitForNextTick() 中的时间计算更加简洁:deadline = tickDuration * (tick + 1),直接对比相对时间即可。
第三,pendingTimeouts 计数器的生命周期管理 。newTimeout() 时原子递增,任务过期执行或取消时在 remove() 中递减。maxPendingTimeouts 的背压机制(默认 -1 不限制)防止了任务无限积压导致 OOM。这个设计类似于线程池的拒绝策略,Netty 在 4.0 之后引入,官方文档建议在需要严格资源控制的场景下设置合理的上限。
下面这张图将"先入队再转移"的解耦设计可视化,展示了调用方线程与 Worker 线程之间的协作关系:

上图的核心在于:调用方线程仅负责参数校验、deadline 计算和 MPSC 入队,全程无锁、零阻塞;Worker 线程在每个 tick 中批量 poll 队列,将任务"转移"到对应 bucket。这种"生产---转移"解耦的设计,使得 newTimeout() 的调用开销极低,即使在高并发场景下也能保持稳定的吞吐。
四、Worker 驱动引擎:run() 主循环
Worker 是 HashedWheelTimer 的核心驱动引擎,它实现了 Runnable 接口,运行在独立的 workerThread 中。整个时间轮的运转完全由 Worker.run() 方法驱动,让我们先通过时序图建立全局认知:

上面的时序图清晰地展示了 Worker.run() 的四个步骤:等待 tick 到达 → 处理取消任务 → 批量转移新任务 → 执行到期任务。每个 tick 周期内,这四步按固定顺序执行,构成了时间轮完整的调度闭环。接下来我们阅读 run() 方法的源码实现。
4.1 run() 的完整源码
java
io.netty.util.HashedWheelTimer.Worker#run
@Override
public void run() {
// 初始化 startTime,作为所有 deadline 计算的基准时间
startTime = System.nanoTime();
if (startTime == 0) {
// 0 在 start() 中被用作"未初始化"的标记,确保不为 0
startTime = 1;
}
// 通知 start() 中的等待者:startTime 已初始化
startTimeInitialized.countDown();
do {
// 步骤①:等待下一个 tick 的 deadline 到达
final long deadline = waitForNextTick();
if (deadline > 0) {
// 步骤②:处理取消队列中的任务
int idx = (int) (tick & mask);
processCancelledTasks();
HashedWheelBucket bucket =
wheel[idx];
// 步骤③:将 timeouts 队列中的新任务批量转入 bucket
transferTimeoutsToBuckets();
// 步骤④:执行当前 bucket 中的到期任务
bucket.expireTimeouts(deadline);
// tick 指针推进
tick++;
}
} while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);
// 循环退出后的清理:收集所有未处理的任务
for (HashedWheelBucket bucket: wheel) {
bucket.clearTimeouts(unprocessedTimeouts);
}
for (;;) {
HashedWheelTimeout timeout = timeouts.poll();
if (timeout == null) {
break;
}
if (!timeout.isCancelled()) {
unprocessedTimeouts.add(timeout);
}
}
processCancelledTasks();
}
run() 方法的结构非常清晰:一个 do-while 主循环驱动四个步骤的反复执行,循环退出后有两段清理逻辑。注意 idx 的计算在 processCancelledTasks() 之前,但 transferTimeoutsToBuckets() 和 expireTimeouts() 之间没有依赖关系------idx 仅用于定位当前 tick 对应的 bucket,transferTimeoutsToBuckets() 会将任务分配到各自的目标 bucket,而非当前 bucket。
4.2 主循环的四步衔接
四步操作在 run() 中按固定顺序执行,构成了时间轮完整的调度闭环。下面这张图将每一步的输入输出和关键逻辑可视化:

这张图清晰地展示了四步之间的数据流向:① 输出 deadline 作为当前 tick 的时间基准;② 在转移前清理取消任务,避免它们在后续步骤中被误执行;③ 将新任务从 timeouts 队列批量转入对应 bucket;④ 以 deadline 为基准,遍历当前 bucket 链表并执行到期任务。四步的先后顺序是精心设计的------先清理取消、再转移新任务、最后执行到期,确保了状态的一致性。
第一步、waitForNextTick() 负责时间轮的精确定时。它计算当前 tick 应达到的 deadline,通过 Thread.sleep() 等待至该时刻。如果等待期间收到 SHUTDOWN 中断信号,返回 Long.MIN_VALUE 终止循环。这一步的细节将在第五章详细展开。
第二步、processCancelledTasks() 负责处理 cancelledTimeouts 队列中的已取消任务。它遍历队列,对每个取消的任务调用 removeAfterCancellation(),将其从 bucket 链表中移除并递减 pendingTimeouts 计数器。这一步放在 transferTimeoutsToBuckets() 之前,确保已取消的任务不会在后续的 expireTimeouts() 中被误执行。
第三步、transferTimeoutsToBuckets() 负责将 timeouts 队列中的新任务批量转入对应的 bucket。它每次 tick 最多转移 100000 个任务,防止调用方线程恶意提交大量任务导致 Worker 线程饥饿。这一步放在 expireTimeouts() 之前,确保新提交的任务在当前 tick 就能被检查是否到期。
第四步、bucket.expireTimeouts(deadline) 负责遍历当前 tick 对应 bucket 中的链表,检查每个任务的 remainingRounds 和 deadline,对到期任务执行 expire()。这一步是整个时间轮的核心输出------将到期任务从 bucket 中移除并提交给 taskExecutor 执行。
4.3 退出后的清理
当 stop() 被调用后,workerState 被 CAS 更新为 SHUTDOWN,do-while 循环退出。此时 Worker 需要收集所有尚未处理的任务,以便 stop() 返回给调用方:
第一段清理遍历所有 bucket,调用 clearTimeouts() 收集每个 bucket 中未过期且未取消的任务。clearTimeouts() 内部调用 pollTimeout() 逐个弹出 bucket 链表中的任务,跳过已过期或已取消的,将剩余任务加入 unprocessedTimeouts 集合。
第二段清理遍历 timeouts 队列,将尚未被 transferTimeoutsToBuckets() 处理的任务也收集起来。最后再调用一次 processCancelledTasks(),确保清理过程中产生的取消请求也被处理。
五、时间轮精度控制:waitForNextTick() 与转移任务
在上一章了解了 Worker.run() 的整体框架之后,本章深入分析两个关键方法的实现细节:waitForNextTick() 的精度控制,以及 transferTimeoutsToBuckets() 的批量转移逻辑。
5.1 waitForNextTick():精度、等待与平台兼容
waitForNextTick() 负责计算当前 tick 应达到的 deadline 并通过 Thread.sleep() 精确等待。它的返回值决定了后续步骤是否执行:返回 > 0 表示正常到达,返回 Long.MIN_VALUE 表示收到 SHUTDOWN 信号。
java
io.netty.util.HashedWheelTimer.Worker#waitForNextTick
private long waitForNextTick() {
// 计算当前 tick 应达到的 deadline(相对于 startTime)
long deadline = tickDuration * (tick + 1);
for (;;) {
// 当前已流逝的时间(相对于 startTime)
final long currentTime = System.nanoTime() - startTime;
// 纳秒 → 毫秒,+999999 实现向上取整
long sleepTimeMs = (deadline - currentTime + 999999) / 1000000;
if (sleepTimeMs <= 0) {
// 当前时间已超过 deadline,立即返回,不阻塞
if (currentTime == Long.MIN_VALUE) {
return -Long.MAX_VALUE;
} else {
return currentTime;
}
}
// Windows 平台兼容:规避 JVM Thread.sleep() 精度 bug
if (PlatformDependent.isWindows()) {
sleepTimeMs = sleepTimeMs / 10 * 10;
if (sleepTimeMs == 0) {
sleepTimeMs = 1;
}
}
try {
Thread.sleep(sleepTimeMs);
} catch (InterruptedException ignored) {
// 检查是否收到了 SHUTDOWN 信号
if (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_SHUTDOWN) {
return Long.MIN_VALUE;
}
}
}
}
这个方法的精度控制体现在三个细节上:
第一,deadline 的计算公式。deadline = tickDuration * (tick + 1) 是基于 startTime 的相对时间,而非绝对时间戳。这意味着即使系统时间被调整,时间轮的 tick 节奏也不会受影响。tick 从 0 开始,第一次 tick 的 deadline 就是 tickDuration,第二次是 2 * tickDuration,以此类推。
第二,sleepTimeMs 的向上取整计算。(deadline - currentTime + 999999) / 1000000 中加 999999 是为了在纳秒到毫秒的转换中向上取整,避免 sleepTimeMs 为 0 时 sleep(0) 的空转。例如,deadline - currentTime = 500000 纳秒(0.5ms),(500000 + 999999) / 1000000 = 1,会 sleep 1ms;如果不等式不加 999999,结果是 0ms,等于不 sleep,Worker 线程会进入忙等。
第三,Windows 平台兼容。sleepTimeMs = sleepTimeMs / 10 * 10 将 sleep 时间向下取整到 10ms 的倍数,这是为了规避 Windows 平台 JVM 的 Thread.sleep() 精度 bug。Windows 的 Thread.sleep(1) 实际可能 sleep 10ms 以上,这个 workaround 将 sleep 时间对齐到 10ms 的倍数,避免不必要的精度误差。
5.2 transferTimeoutsToBuckets():批量转移的核心逻辑
transferTimeoutsToBuckets() 在每个 tick 中批量将 timeouts 队列中的任务转入对应的 bucket。它的核心逻辑是计算 remainingRounds 和 stopIndex:
java
io.netty.util.HashedWheelTimer.Worker#transferTimeoutsToBuckets
private void transferTimeoutsToBuckets() {
// 每次 tick 最多转移 100000 个任务,防止 Worker 线程饥饿
for (int i = 0; i < 100000; i++) {
HashedWheelTimeout timeout = timeouts.poll();
if (timeout == null) {
// 队列已空,所有任务处理完毕
break;
}
if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) {
// 任务在入队后、转移前被取消,跳过
continue;
}
// 计算任务在时间轮上的目标 tick 位置
long calculated = timeout.deadline / tickDuration;
// 计算还需要经过多少轮才能到期
timeout.remainingRounds = (calculated - tick) / wheel.length;
// 确保不调度到过去的位置(取 max(calculated, tick))
final long ticks = Math.max(calculated, tick);
// 位掩码定位目标 bucket 索引
int stopIndex = (int) (ticks & mask);
HashedWheelBucket bucket = wheel[stopIndex];
bucket.addTimeout(timeout);
}
}
这段代码中有两个关键的计算公式:
calculated = timeout.deadline / tickDuration 计算出任务在绝对 tick 坐标上的位置。例如,deadline = 1500ms,tickDuration = 100ms,则 calculated = 15,表示任务应该在时间轮的第 15 个 tick 到期。
remainingRounds = (calculated - tick) / wheel.length 计算出任务还需要经过多少轮才能到期。例如,calculated = 15,tick = 3,wheel.length = 8,则 remainingRounds = (15 - 3) / 8 = 1,表示任务还需要经过 1 轮(即 tick 到达 15 时)才到期。当前 tick 指向 3,remainingRounds = 1 说明任务在下一轮的同位置到期。
stopIndex = max(calculated, tick) & mask 确保任务不会被调度到过去的位置。Math.max(calculated, tick) 防止了 calculated < tick 的情况(即任务已经过期),此时会将任务放入当前 tick 对应的 bucket,remainingRounds = 0,在下一次 expireTimeouts() 中立即执行。
最后,每 tick 最多转移 100000 个任务的限制是一个重要的保护机制。注释中明确说明:"防止调用方线程在一个循环中不断添加新任务,导致 Worker 线程饥饿"。如果没有这个限制,恶意的调用方可以通过无限提交任务,让 transferTimeoutsToBuckets() 永远无法返回,后续的 expireTimeouts() 无法执行,所有已到期的任务都将被延迟。
六、Bucket 内部:HashedWheelTimeout 的状态管理与过期执行
在前几章中,我们看到了 HashedWheelTimeout 作为一个"任务句柄"在时间轮中的流转------从 newTimeout() 创建,到 transferTimeoutsToBuckets() 入桶,再到 expireTimeouts() 中到期执行。本章深入分析 HashedWheelTimeout 的内部状态机和 HashedWheelBucket 的链表操作。
6.1 HashedWheelTimeout 的字段与状态机
HashedWheelTimeout 同时实现了 Timeout 和 Runnable 两个接口,既是调用方控制任务的句柄,也是任务到期后的执行载体。它的三态状态机如下:

状态转换通过 AtomicIntegerFieldUpdater 原子更新,确保 cancel() 和 expire() 在多线程下的安全性:
java
io.netty.util.HashedWheelTimer.HashedWheelTimeout
private static final int ST_INIT = 0;
private static final int ST_CANCELLED = 1;
private static final int ST_EXPIRED = 2;
private static final AtomicIntegerFieldUpdater<HashedWheelTimeout> STATE_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(HashedWheelTimeout.class, "state");
private final HashedWheelTimer timer;
private final TimerTask task;
private final long deadline;
// 状态字段,通过 STATE_UPDATER 原子更新(volatile 保证可见性)
@SuppressWarnings({"unused", "FieldMayBeFinal", "RedundantFieldInitialization" })
private volatile int state = ST_INIT;
// remainingRounds 在 transferTimeoutsToBuckets() 中计算并设置
long remainingRounds;
// 双向链表指针,仅在 Worker 线程中操作,无需同步
HashedWheelTimeout next;
HashedWheelTimeout prev;
// 所属 bucket 引用,仅在 Worker 线程中操作
HashedWheelBucket bucket;
remainingRounds 的设计值得单独说明。在单层时间轮中,当任务的延迟时间超过 wheel.length * tickDuration 时,任务无法在单轮内到期。remainingRounds 记录了任务还需要经过多少轮才能到期。Worker 线程在 expireTimeouts() 中遍历 bucket 链表时,对 remainingRounds > 0 的任务仅执行 remainingRounds--,对 remainingRounds <= 0 的任务才执行 expire()。这个机制巧妙地将单层时间轮的覆盖范围扩展到了任意长度,无需引入多层时间轮。
6.2 cancel() 的"延迟取消"机制
cancel() 方法的实现体现了 Netty 对"取消"操作的设计哲学------不在调用方线程中直接操作 bucket 链表,而是通过 cancelledTimeouts 队列将取消操作"延迟"到 Worker 线程中处理:
java
io.netty.util.HashedWheelTimer.HashedWheelTimeout#cancel
@Override
public boolean cancel() {
// CAS 原子转换 ST_INIT → ST_CANCELLED,防止重复取消
if (!compareAndSetState(ST_INIT, ST_CANCELLED)) {
return false;
}
// 不立即从 bucket 移除,而是加入 cancelledTimeouts 队列
// Worker 线程在下一个 tick 的 processCancelledTasks() 中处理
// 延迟 = 最多 1 个 tick 间隔
timer.cancelledTimeouts.add(this);
return true;
}
cancel() 只做两件事:CAS 将状态从 ST_INIT 转为 ST_CANCELLED,然后将 this 加入 cancelledTimeouts 队列。它不直接操作 bucket 链表,原因有二:第一,bucket 链表只在 Worker 线程中操作,调用方线程直接操作需要加锁,而 CAS 入队是无锁的;第二,如果任务已被 expireTimeouts() 遍历到但尚未执行 expire(),此时 cancel() 的 CAS 会失败(因为 expire() 也会 CAS ST_INIT → ST_EXPIRED),从而保证任务不会被重复执行。
取消的延迟最多为 1 个 tick 间隔。Worker 线程在每个 tick 的 processCancelledTasks() 中统一处理:
java
io.netty.util.HashedWheelTimer.HashedWheelTimeout#removeAfterCancellation
void removeAfterCancellation() {
// 从 bucket 链表中移除
remove();
// 回调 task.cancelled(this),通知业务层
task.cancelled(this);
}
java
io.netty.util.HashedWheelTimer.HashedWheelTimeout#remove
private void remove() {
HashedWheelBucket bucket = this.bucket;
if (bucket != null) {
bucket.remove(this);
}
// 递减 pendingTimeouts 计数器
timer.pendingTimeouts.decrementAndGet();
}
6.3 expire() 的 CAS 防护与执行路径
expire() 方法在 expireTimeouts() 中被调用,负责将到期的任务提交给 taskExecutor 执行:
java
io.netty.util.HashedWheelTimer.HashedWheelTimeout#expire
public void expire() {
// CAS 防护:只有 ST_INIT → ST_EXPIRED 成功才执行
// 如果任务已被 cancel() 转为 ST_CANCELLED,CAS 失败,直接返回
if (!compareAndSetState(ST_INIT, ST_EXPIRED)) {
return;
}
try {
// 从 bucket 链表中移除
remove();
// 提交给 taskExecutor 执行
timer.taskExecutor.execute(this);
} catch (Throwable t) {
if (logger.isWarnEnabled()) {
logger.warn("An exception was thrown while submit " + TimerTask.class.getSimpleName()
+ " for execution.", t);
}
}
}
expire() 中 CAS 防护的意义在于防止"已取消的任务被误执行"。考虑以下场景:任务 T1 在 expireTimeouts() 中被遍历到,remainingRounds = 0,即将执行 expire();与此同时,调用方线程调用了 cancel()。如果 cancel() 先执行(CAS ST_INIT → ST_CANCELLED),expire() 的 CAS 会失败,任务不会被执行;如果 expire() 先执行(CAS ST_INIT → ST_EXPIRED),cancel() 的 CAS 会失败,返回 false 表示取消失败。无论哪种顺序,结果都是正确的。
expire() 执行后,HashedWheelTimeout 自身作为 Runnable 被提交给 taskExecutor。默认的 ImmediateExecutor.INSTANCE 直接在 Worker 线程中执行:
java
io.netty.util.HashedWheelTimer.HashedWheelTimeout#run
@Override
public void run() {
try {
// 调用业务层的 task.run(timeout),传递 Timeout 句柄
task.run(this);
} catch (Throwable t) {
if (logger.isWarnEnabled()) {
logger.warn("An exception was thrown by " + TimerTask.class.getSimpleName() + '.', t);
}
}
}
6.4 HashedWheelBucket 的链表操作
HashedWheelBucket 是时间轮的存储单元,内部维护一个 HashedWheelTimeout 的双向链表。所有链表操作都在 Worker 线程中执行,无需同步。其内部结构如下:

addTimeout() 采用尾插法,时间复杂度 O(1):
java
io.netty.util.HashedWheelTimer.HashedWheelBucket#addTimeout
public void addTimeout(HashedWheelTimeout timeout) {
assert timeout.bucket == null;
// 设置 timeout 的 bucket 引用
timeout.bucket = this;
if (head == null) {
// 链表为空,head 和 tail 都指向该 timeout
head = tail = timeout;
} else {
// 尾插法:追加到链表末尾
tail.next = timeout;
timeout.prev = tail;
tail = timeout;
}
}
remove() 支持从链表中任意位置移除节点,时间复杂度 O(1)(双向链表的优势):
java
io.netty.util.HashedWheelTimer.HashedWheelBucket#remove
public HashedWheelTimeout remove(HashedWheelTimeout timeout) {
HashedWheelTimeout prev = timeout.prev;
HashedWheelTimeout next = timeout.next;
// 更新前后节点的指针
if (prev != null) {
prev.next = next;
}
if (next != null) {
next.prev = prev;
}
// 更新 head 和 tail 指针
if (timeout == head) {
head = next;
}
if (timeout == tail) {
tail = prev;
}
// 清空引用,帮助 GC 回收
timeout.prev = null;
timeout.next = null;
timeout.bucket = null;
return next;
}
expireTimeouts() 遍历整个链表,对到期任务执行 expire():
java
io.netty.util.HashedWheelTimer.HashedWheelBucket#expireTimeouts
public void expireTimeouts(long deadline) {
HashedWheelTimeout timeout = head;
while (timeout != null) {
HashedWheelTimeout next = timeout.next;
if (timeout.remainingRounds <= 0) {
if (timeout.deadline <= deadline) {
// remainingRounds = 0 且 deadline 已到,执行 expire()
timeout.expire();
} else {
// timeout 被放入了错误的 slot,不应该发生
throw new IllegalStateException(String.format(
"timeout.deadline (%d) > deadline (%d)", timeout.deadline, deadline));
}
} else if (!timeout.isCancelled()) {
// remainingRounds > 0 且未取消,递减剩余轮数
timeout.remainingRounds --;
}
timeout = next;
}
}
注意遍历时先保存 next = timeout.next,再对 timeout 执行 expire()。这是因为 expire() 内部会调用 remove(),将 timeout 从链表中移除,timeout.next 会被置为 null。先保存 next 再操作,保证了遍历的正确性。
七、停止与回收:stop() 的优雅关闭
HashedWheelTimer 的 stop() 方法负责优雅地关闭时间轮,包括停止 Worker 线程、收集未处理任务、清理资源泄漏追踪等。它的实现分为 CAS 状态转换、Worker 线程中断、未处理任务回收三个阶段。
java
io.netty.util.HashedWheelTimer#stop
@Override
public Set<Timeout> stop() {
// 阶段①:防护检查------禁止 Worker 线程自己调用 stop()
if (Thread.currentThread() == workerThread) {
throw new IllegalStateException(
HashedWheelTimer.class.getSimpleName() +
".stop() cannot be called from " +
TimerTask.class.getSimpleName());
}
// 阶段②:CAS 状态转换 STARTED → SHUTDOWN
if (!WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_STARTED, WORKER_STATE_SHUTDOWN)) {
// CAS 失败说明已被关闭或从未启动,直接设为 SHUTDOWN 并返回空集合
if (WORKER_STATE_UPDATER.getAndSet(this, WORKER_STATE_SHUTDOWN) != WORKER_STATE_SHUTDOWN) {
INSTANCE_COUNTER.decrementAndGet();
if (leak != null) {
boolean closed = leak.close(this);
assert closed;
}
}
return Collections.emptySet();
}
// 阶段③:等待 Worker 线程退出
try {
boolean interrupted = false;
while (workerThread.isAlive()) {
// 中断 Worker 线程,触发 waitForNextTick() 中的 InterruptedException
workerThread.interrupt();
try {
// join(100) 限时等待,避免死等
workerThread.join(100);
} catch (InterruptedException ignored) {
interrupted = true;
}
}
if (interrupted) {
// 恢复中断标志,遵循 Java 中断最佳实践
Thread.currentThread().interrupt();
}
} finally {
// 递减全局实例计数
INSTANCE_COUNTER.decrementAndGet();
if (leak != null) {
boolean closed = leak.close(this);
assert closed;
}
}
// 阶段④:获取并返回未处理的任务
Set<Timeout> unprocessed = worker.unprocessedTimeouts();
Set<Timeout> cancelled = new HashSet<Timeout>(unprocessed.size());
for (Timeout timeout : unprocessed) {
if (timeout.cancel()) {
cancelled.add(timeout);
}
}
return cancelled;
}
stop() 的实现中有几个值得关注的细节:
其一,禁止 Worker 线程自己调用 stop()。这是因为 stop() 内部会调用 workerThread.join(),如果 Worker 线程自己调用 stop(),会自己 join 自己,导致死锁。这个检查放在方法的最前面,是典型的防御性编程。
其二,CAS 失败后的幂等处理。compareAndSet(STARTED, SHUTDOWN) 失败后,代码通过 getAndSet(this, SHUTDOWN) 将状态强制设为 SHUTDOWN,并检查是否之前已经是 SHUTDOWN。如果是(即 != SHUTDOWN 为 false),说明 stop() 已经被调用过,当前是重复调用,不需要重复递减 INSTANCE_COUNTER 和关闭 leak。
其三,interrupt() + join(100) 的循环等待模式。Worker 线程在 waitForNextTick() 中 sleep() 时,收到 interrupt() 信号后会检查 workerState 是否为 SHUTDOWN,若是则返回 Long.MIN_VALUE 终止主循环。join(100) 限时等待 100ms 后重新检查 workerThread.isAlive(),如果 Worker 线程仍在执行某种长耗时任务(如 expireTimeouts() 中的业务逻辑),则再次 interrupt()。
其四,finalize() 的兜底保护。finalize() 在 HashedWheelTimer 实例被 GC 回收时触发,将 workerState 设为 SHUTDOWN 并递减 INSTANCE_COUNTER。但需要注意的是,Worker 是 HashedWheelTimer 的非静态内部类,持有外部实例的隐式引用,而 workerThread 又持有 Worker 的引用------只要 Worker 线程仍在运行,HashedWheelTimer 实例就处于可达状态,不会被 GC 回收,finalize() 也不会被触发。因此,忘记调用 stop() 会直接导致 Worker 线程持续运行直至 JVM 退出,finalize() 无法对此兜底。
7.1 时间轮的局限性
在深入理解了 HashedWheelTimer 的完整实现之后,我们需要客观地认识它的局限性:
第一,精度受限于 tickDuration。HashedWheelTimer 的最小精度是 tickDuration(默认 100ms),不适合需要精确到毫秒甚至纳秒级别的定时场景。例如,delay = 50ms 的任务,实际可能在第 0 个 tick(立即)或第 1 个 tick(100ms 后)被执行,取决于任务提交时距离下一个 tick 还有多久。
第二,单层时间轮的时间范围有限。wheel.length * tickDuration 决定了单层能覆盖的最大时间范围。默认 512 个 bucket * 100ms = 51.2 秒,超过这个范围的任务需要 remainingRounds 来补偿,但 remainingRounds 越大,任务在 expireTimeouts() 中被遍历的次数越多(每次 tick 都会递减一次),存在一定的性能开销。
第三,任务执行时间过长会阻塞后续 tick。默认 taskExecutor 是 ImmediateExecutor.INSTANCE,过期任务在 Worker 线程中直接执行。如果一个任务的 run() 方法耗时 200ms,而 tickDuration 只有 100ms,那么后续的 tick 会被延迟,时间轮的精度会进一步退化。调用方可以通过自定义 taskExecutor 将任务提交到独立线程池来解决此问题。
7.2 HashedWheelTimer 的定位与使用场景
需要说明的是,在 Netty 4.2.x 中,IdleStateHandler、ReadTimeoutHandler、WriteTimeoutHandler 等内置 timeout handler 已经不再使用 HashedWheelTimer,而是改为使用 ctx.executor().schedule() 在 EventLoop 中直接调度定时任务。ReadTimeoutHandler 继承自 IdleStateHandler,后者的 schedule() 方法直接调用 ctx.executor().schedule(task, delay, unit),将空闲检测任务注册到 Channel 绑定的 EventLoop 上,而非依赖外部共享的 Timer 线程。
HashedWheelTimer 在 Netty 4.2.x 中仍然作为 common 模块的公共 API 保留,适用于需要独立于 EventLoop 的定时调度场景------例如应用层自定义的定时重试、批量任务调度、或需要跨 Channel 共享的全局定时器。它的核心价值在于:当定时任务的数量庞大且不需要纳秒级精度时,HashedWheelTimer 的 O(1) 插入和遍历开销优于 JDK ScheduledExecutorService 的 O(log n) 堆操作。
八、整体链路串联
将前面各章的内容串联起来,HashedWheelTimer 的完整生命周期是这样的:
任务提交阶段,调用方通过 newTimeout() 提交任务,pendingTimeouts 原子递增,start() 确保 Worker 线程已启动,计算 deadline 后创建 HashedWheelTimeout 并加入 timeouts 队列(MPSC 无锁队列)。此时任务尚未进入时间轮,调用方线程立即返回,零阻塞。
时间轮驱动阶段,Worker 线程在 run() 中进入死循环,每个 tick 执行四步:waitForNextTick() 精确等待至下一个 tick 的 deadline;processCancelledTasks() 处理取消队列中的任务,从 bucket 链表中移除;transferTimeoutsToBuckets() 批量将 timeouts 队列中的任务转入对应的 bucket(每 tick 最多 100000 个),计算 remainingRounds 和 stopIndex;expireTimeouts() 遍历当前 tick 对应 bucket 的链表,对 remainingRounds <= 0 且 deadline <= deadline 的任务执行 expire()。
到期执行阶段,expire() 通过 CAS ST_INIT → ST_EXPIRED 确保任务只被执行一次,调用 remove() 从 bucket 链表中移除,然后通过 taskExecutor.execute(this) 提交执行。HashedWheelTimeout 自身作为 Runnable,在 run() 中回调 task.run(this)。
优雅关闭阶段,stop() 通过 CAS 将 workerState 设为 SHUTDOWN,interrupt() Worker 线程触发 waitForNextTick() 中的 InterruptedException,Worker 退出主循环后收集所有未处理任务,stop() 返回成功取消的任务集合。
HashedWheelTimer 的设计思想可以凝练为:"环形数组 + 延迟批量转移 + MPSC 无锁队列 "。三个核心设计决策贯穿始终:第一,任务先入 MPSC 队列再批量转移------调用方线程零阻塞,Worker 线程批量处理,两端解耦;第二,remainingRounds 字段解决多轮覆盖问题------无需遍历所有 bucket,每个 tick 只检查当前 bucket 的链表,未到期的任务仅递减 remainingRounds;第三,CAS 状态机 + 延迟取消------避免锁竞争,Worker 线程统一处理状态变更,取消操作延迟最多 1 个 tick 间隔。
为了更直观地理解 HashedWheelTimer 与传统 JDK 定时方案的区别,下面做一个对比总结:
| 维度 | JDK ScheduledExecutorService | HashedWheelTimer |
|---|---|---|
| 数据结构 | DelayedWorkQueue(二叉堆) | HashedWheelBucket\[\](环形数组) |
| 插入复杂度 | O(log n) | O(1)(入队 MPSC 队列) |
| 到期检测 | 每次从堆顶取最小 deadline | 每个 tick 遍历当前 bucket 链表 |
| 精度 | 纳秒级 | tick 级(默认 100ms) |
| 取消机制 | Future.cancel() 立即生效 | CAS 标记 + 延迟清理(最多 1 tick) |
| 适用场景 | 精确延迟任务 | 海量近似精度 IO 超时 |
全文小结
本文聚焦 Netty HashedWheelTimer 的源码实现,从时间轮数据结构和 Worker 驱动引擎两个维度,深入分析了其用环形数组替代优先队列、O(1) 任务调度、MPSC 无锁批量转移的设计原理。
在数据结构方面,HashedWheelBucket[] 环形数组通过 tick & mask 位运算实现 O(1) 定位,HashedWheelTimeout 的双向链表结构支持 O(1) 的插入和移除,remainingRounds 字段巧妙地解决了单层时间轮的多轮覆盖问题。在驱动引擎方面,Worker.run() 的 waitForNextTick() → processCancelledTasks() → transferTimeoutsToBuckets() → expireTimeouts() 四步循环构成了时间轮的核心调度逻辑,其中 transferTimeoutsToBuckets() 的批量转移(每 tick 最多 100000 个)确保了调用方线程零阻塞,processCancelledTasks() 的延迟取消机制避免了锁竞争。在生命周期管理方面,stop() 通过 CAS 状态机 + interrupt() + join() 实现优雅关闭,ResourceLeakDetector 提供了资源泄漏检测的兜底保障。
原创不易,如果本文对您有帮助,带来了些许灵感或启发,烦请动动小手点赞、关注、转发、收藏。这是作者持续更新的动力源泉,衷心感谢您的支持。我会尽量在工作之余,为大家带来更高品质的内容,努力保持周更。