背景
说到定时任务,笔者接触过的就有crontable
,spirng-schedule
,quartz
实现有很多方式,但是底层都依赖操作系统的中断,今天我们要介绍一款比较实用,并且面试加分的定时任务时间轮
,时间轮算法出自一篇论文《Hashed and Hierarchical Timing Wheels》,当然在java框架中也有大牛落地,比如netty
,dubbo
等。
设计
我们先说说他的设计,设计和现实生活中的时钟很接近,分钟指针从0匀速转到12,是一个小时,然后我们联想下hashmap,如果hash槽长度是12,每个槽用list存储定时任务。接下来我们去实现一种比较简单的方法。
设计图
说明下上图的结构,有几个概念,任务管理器
:MyHashedWheelTimer 是任务提交和执行的容器,任务队列
:HashedWheelBucket 是同一个hash槽中的任务组装容器,HashedWheelTimeout 包装了每个任务扩展pre与next和任务执行时间等,里面包含了最小原子的任务TimerTask
,一个周期中指针跳动一次的时间长度为tickDuration
。
代码编写
我们根据图上的设计把几个类模型建立下。
public class MyHashedWheelTimer {
//调用定时任务的执行对象
private final Worker worker = new Worker();
/**
* 定时任务管理器开始的毫秒时刻
*/
private volatile long startTime;
//指针运行一圈走过的点数
private final int mask;
private final HashedWheelBucket[] wheel;
/**
* 时钟转动的最小单位 毫秒
*/
private final long tickDuration;
//根据指针跳转一次的时间来初始化 任务管理器
public MyHashedWheelTimer(long tickDuration) {}
//将超时任务`task` 添加到任务管理器 以供后续触发
public HashedWheelTimeout newTimeout(Runnable task, long delay, TimeUnit unit){}
}
private static final class HashedWheelBucket {
private HashedWheelTimeout head;//队列头
private HashedWheelTimeout tail;//队列尾
//将超时任务添加到hash槽的队列中
void addTimeout(HashedWheelTimeout timeout) {}
//执行超时任务
void doTimeouts(long deadline, long tick) {}
//执行完任务,移除任务
public HashedWheelTimeout remove(HashedWheelTimeout timeout) {}
}
private final class Worker implements Runnable {
private long tick;//指针跳转的次数,随着任务管理器启动之后就一直叠加
@Override
public void run() {
do {
final long deadline = waitForNextTick();//指针每跳一次之后的时间
//...找到对应任务,然后执行
} while (true);
}
private long waitForNextTick() {
//指针跳转一次
}
}
//将任务和任务的执行时间包装
private static final class HashedWheelTimeout {
long remainingRounds;//任务会在时钟从启动到转到多少次后执行
HashedWheelTimeout next;
HashedWheelTimeout prev;
HashedWheelBucket bucket;
long deadline;
Runnable task;
HashedWheelTimeout(Runnable task, long deadline) {
this.task = task;
this.deadline = deadline;
}
}
上面4个类实现了最基本的功能,下面我们补充下具体实现
public MyHashedWheelTimer(long tickDuration) {
wheel = createWheel(64);//默认一周期 64个单元
mask = wheel.length - 1;//查找任务所在的hash环位置用 例如 a & mask = idx
this.tickDuration = TimeUnit.MILLISECONDS.toMillis(tickDuration);//跳转一次的时间
startTime = System.currentTimeMillis();//任务管理器启动事件
new Thread(worker).start();//启动任务调用线程
}
//创建每个hash槽上的HashedWheelBucket对象
private static HashedWheelBucket[] createWheel(int ticksPerWheel) {
HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel];
for (int i = 0; i < wheel.length; i++) {
wheel[i] = new HashedWheelBucket();
}
return wheel;
}
public HashedWheelTimeout newTimeout(Runnable task, long delay, TimeUnit unit) {
//次任务在未来哪一毫秒执行
long deadline = System.currentTimeMillis() + unit.toMillis(delay) - startTime;
HashedWheelTimeout timeout = new HashedWheelTimeout(task, deadline);
//任务执行的时间距离任务管理器启动需要多少个周期
timeout.remainingRounds = (deadline / tickDuration);
//任务将会落在hash槽的什么位置索引上
int stopIndex = (int) ((deadline / tickDuration) & mask);
HashedWheelBucket bucket = wheel[stopIndex];
bucket.addTimeout(timeout);//将任务添加到hash槽的链表中
return timeout;
}
private final class Worker implements Runnable {
public void run() {
do {
final long deadline = waitForNextTick();
if (deadline > 0) {
int idx = (int) (tick & mask); // & 64
HashedWheelBucket bucket =
wheel[idx];
bucket.doTimeouts(deadline, tick);//执行该hash槽中满足条件的定时任务
tick++;
}
} while (true);
}
}
void addTimeout(HashedWheelTimeout timeout) {
//双向链表添加节点,不懂可以去看看数据结构操作
}
public HashedWheelTimeout remove(HashedWheelTimeout timeout) {
//双向链表删除节点,不懂可以去看看数据结构操作
}
//遍历链表中的所有任务
void doTimeouts(long deadline, long tick) {
HashedWheelTimeout timeout = head;
while (timeout != null) {
HashedWheelTimeout next = timeout.next;
//如果满足remainingRounds(需要指针跳转的次数)-tick(已经跳转的次数) <=0 则触发任务执行
if (timeout.remainingRounds - tick <= 0) {
next = remove(timeout);
if (timeout.deadline <= deadline) {
timeout.task.run();//调用任务的run()方法,和线程池有异曲同工之妙
} else {
throw new IllegalStateException();
}
}
timeout = next;
}
}
分析
代码中只是基础功能的实现,具体大家可以看看和dubbo 或者 netty 的实现差别在哪,比如任务管理没有启停,task提交了不可以取消,线程不安全,入参不够丰富,任务个数不可控等等。
思考
- 时间轮对执行的任务只会处理一次,怎么实现周期性的任务?
- 如果任务还需要等待很长时间才执行怎么设计?
- 如果任务很多很密集怎么让任务最接近理论执行时间?
总结
代码不全,如需要完整代码可以留言。