在业务开发中,我们经常会遇到需要执行延时任务的场景,比如订单超时自动取消、定时消息推送、会话超时关闭等。当任务规模达到百万级时,传统的 Timer、ScheduledThreadPoolExecutor 等调度工具会因为底层数据结构的时间复杂度问题,出现任务添加、触发效率下降的情况,而基于时间轮算法的 HashedWheelTimer 能够很好地解决这个问题。

时间轮算法核心原理
时间轮的本质是一个环形数组,数组的每个槽位代表一个时间刻度,每个槽位上挂载着该刻度需要触发的延时任务。时间轮会以固定的时间间隔转动,每转动到一个槽位,就触发该槽位上所有到期的任务。
假设时间轮有 N 个槽位,每个槽位代表的时间间隔为 tickDuration,那么时间轮可以覆盖的时间范围是 N * tickDuration。如果任务的延时时间超过了时间轮的覆盖范围,通常会采用多层时间轮的方式,类似时钟的时、分、秒指针,高层时间轮的一个槽位对应低层时间轮的完整一轮,以此扩展可调度的时间范围。
时间轮添加任务的时间复杂度是 O(1),触发任务时只需要遍历当前槽位的任务列表,整体性能不会随着任务数量的增加而明显下降,非常适合海量延时任务的场景。
HashedWheelTimer 基础使用
HashedWheelTimer 是 Netty 提供的时间轮实现,开箱即用,不需要我们手动实现时间轮的底层逻辑。首先我们来看它的核心构造函数参数:
- tickDuration:时间轮每个刻度的间隔,单位由第三个参数指定
- ticksPerWheel:时间轮的槽位数量
- unit:tickDuration 的时间单位
- leakDetection:是否开启内存泄漏检测
简单延时任务示例
下面是一个使用 HashedWheelTimer 执行简单延时任务的示例:
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import java.util.concurrent.TimeUnit;
public class HashedWheelTimerDemo {
public static void main(String[] args) {
// 创建时间轮,每个刻度100ms,共512个槽位,使用毫秒作为时间单位
HashedWheelTimer timer = new HashedWheelTimer(100, TimeUnit.MILLISECONDS, 512);
// 启动时间轮
timer.start();
// 提交一个延时1秒执行的任务
TimerTask task = new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
System.out.println("延时任务执行,当前时间:" + System.currentTimeMillis());
}
};
timer.newTimeout(task, 1, TimeUnit.SECONDS);
// 提交一个延时3秒执行的任务
timer.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
System.out.println("第二个延时任务执行,当前时间:" + System.currentTimeMillis());
// 任务执行完成后关闭时间轮
timer.stop();
}
}, 3, TimeUnit.SECONDS);
}
}
百万级延时任务管理实现
要实现百万级延时任务的高效管理,需要做好任务封装、时间轮参数配置、任务取消和过期处理几个部分。
任务封装设计
我们首先封装一个通用的延时任务类,包含任务ID、执行逻辑、延时时间、取消标记等属性:
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import java.util.concurrent.TimeUnit;
public class DelayTask implements TimerTask {
// 任务唯一ID
private String taskId;
// 任务执行逻辑
private Runnable executeLogic;
// 延时时间
private long delay;
// 时间单位
private TimeUnit timeUnit;
// 任务是否被取消
private volatile boolean cancelled = false;
// 对应的Timeout对象,用于取消任务
private Timeout timeout;
public DelayTask(String taskId, Runnable executeLogic, long delay, TimeUnit timeUnit) {
this.taskId = taskId;
this.executeLogic = executeLogic;
this.delay = delay;
this.timeUnit = timeUnit;
}
@Override
public void run(Timeout timeout) throws Exception {
// 如果任务被取消,不执行逻辑
if (cancelled) {
return;
}
// 执行任务逻辑
executeLogic.run();
}
public void cancel() {
this.cancelled = true;
if (timeout != null) {
timeout.cancel();
}
}
public String getTaskId() {
return taskId;
}
public long getDelay() {
return delay;
}
public TimeUnit getTimeUnit() {
return timeUnit;
}
public void setTimeout(Timeout timeout) {
this.timeout = timeout;
}
}
任务管理器实现
接下来实现一个任务管理器,负责和时间轮交互,管理任务的提交、取消、查询:
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class DelayTaskManager {
// 单例时间轮实例,全局共用一个
private HashedWheelTimer timer;
// 存储任务ID和对应的Timeout,用于取消任务
private Map<String, Timeout> taskMap = new ConcurrentHashMap<>();
public DelayTaskManager(long tickDuration, TimeUnit unit, int ticksPerWheel) {
this.timer = new HashedWheelTimer(tickDuration, unit, ticksPerWheel);
this.timer.start();
}
/**
* 提交延时任务
*/
public void submitTask(DelayTask task) {
// 提交任务到时间轮,获取Timeout对象
Timeout timeout = timer.newTimeout(task, task.getDelay(), task.getTimeUnit());
task.setTimeout(timeout);
// 将任务ID和Timeout存入Map
taskMap.put(task.getTaskId(), timeout);
}
/**
* 取消任务
*/
public boolean cancelTask(String taskId) {
Timeout timeout = taskMap.get(taskId);
if (timeout != null) {
timeout.cancel();
taskMap.remove(taskId);
return true;
}
return false;
}
/**
* 关闭任务管理器
*/
public void shutdown() {
timer.stop();
taskMap.clear();
}
}
百万级任务测试验证
我们可以通过以下代码模拟百万级任务的提交,验证时间轮的性能:
import java.util.concurrent.TimeUnit;
public class MassTaskTest {
public static void main(String[] args) throws InterruptedException {
// 初始化任务管理器,每个刻度10ms,1024个槽位
DelayTaskManager manager = new DelayTaskManager(10, TimeUnit.MILLISECONDS, 1024);
long startTime = System.currentTimeMillis();
int taskCount = 1000000;
// 提交100万个延时任务,延时时间在1-10秒之间随机
for (int i = 0; i < taskCount; i++) {
String taskId = "task_" + i;
long delay = (long) (Math.random() * 9 + 1);
DelayTask task = new DelayTask(taskId, () -> {
// 任务执行逻辑,这里仅打印日志
// System.out.println("任务" + taskId + "执行");
}, delay, TimeUnit.SECONDS);
manager.submitTask(task);
}
long endTime = System.currentTimeMillis();
System.out.println("提交" + taskCount + "个任务耗时:" + (endTime - startTime) + "ms");
// 等待所有任务执行完成
Thread.sleep(11000);
manager.shutdown();
}
}
实际测试中,提交百万级任务的总耗时通常在几百毫秒级别,远优于传统调度工具的性能表现。
注意事项和适用场景
使用 HashedWheelTimer 时需要注意几个问题:
- 时间轮的精度由 tickDuration 决定,tickDuration 越小精度越高,但转动频率越高,CPU消耗也越大,需要根据业务需求平衡
- HashedWheelTimer 是单线程执行任务的,如果单个任务执行时间过长,会阻塞后续任务的触发,因此任务逻辑要尽量简短,耗时操作可以提交到其他线程池执行
- 时间轮适合管理大量短延时的任务,如果任务延时时间跨度非常大,多层时间轮的配置会比较复杂,此时可以考虑其他调度方案
适用场景主要包括:短连接超时管理、订单超时取消、定时消息推送、限流器的时间窗口统计等需要大量延时任务调度的场景。
HashedWheelTimer时间轮算法延时任务任务调度修改时间:2026-07-14 01:03:35