时间轮是一种可以执行定时任务的数据结构和算法.这篇文章,讲解一下它在Netty 3.x系列中如何实现的,它在4.x系列将在后面的文章中讲解.
此次讲解的版本如下
代码语言:javascript复制<dependency>
<groupId>org.jboss.netty</groupId>
<artifactId>netty</artifactId>
<version>3.2.5.Final</version>
</dependency>
它的实现上是一个一维数组,但把它当成一个环形数组使用.如同时间的车轮一样.
首先说明一点的是,时间轮是按照顺时针的方向'转动',也就是按照顺时针的方向执行时间轮上的任务.
每一个格子的时间跨度都是tickDuration,就是说图中每个小格子的时间长度都是tickDuration.
时间轮在开始'转动'的时候会记录下开始时间startTime.每个格子表示一个tick值,第一个格子的tick值等于1,第二个格子的tick值等于2,以此类推.
如图所示,第9个格子与第1个格子是同一个,第10个格子与第2个是同一个,以此类推.
根据上面的几点说明,计算图中K点的时刻
如果tick=1,那么结果等于startTime tickDuration * 1
如果tick=9,那么结果等于startTime tickDuration * 9
想想是不是,因为每个格子的时间跨度都是tickDuration,所以经过n个格子,就是tickDuration * n这么长时间,又由于起始时间是startTime,所以每个格子的结束时刻(例如图中K点,Q点)就等于startTime tickDuration * n
时间轮初始化之后,它的结构如下图
假如此时时间轮正在执行下图中S格子中的任务,这时向时间轮中添加一个延时delay的任务,时间轮会根据当前所处的位置和时刻,计算新添加的任务应该放在哪个格子位置.而且新添加的任务的deadline=currentTime delay.也就是说,当前时间加上dalay延时时间就是这个任务要在那个时刻被执行.
时间轮会绑定一个唯一的线程,执行时间轮上的任务.
代码语言:javascript复制// 构造器
public HashedWheelTimer(
ThreadFactory threadFactory,
long tickDuration, TimeUnit unit, int ticksPerWheel) {
wheel = createWheel(ticksPerWheel);// 创建时间轮,实际就是一个Set数组
iterators = createIterators(wheel);// 创建每个Set元素的迭代器
mask = wheel.length - 1;
this.tickDuration = tickDuration = unit.toMillis(tickDuration);// 每个格子的时间跨度
roundDuration = tickDuration * wheel.length; // 转一圈的时间
// 执行时间轮里面任务的线程
workerThread = threadFactory.newThread(new ThreadRenamingRunnable(worker, "Hashed wheel timer #" id.incrementAndGet()));
}
代码语言:javascript复制// 源码位置: org.jboss.netty.util.HashedWheelTimer.Worker#run
public void run() {
List<HashedWheelTimeout> expiredTimeouts = new ArrayList<HashedWheelTimeout>();
// 记录时间轮启动的开始时间
startTime = System.currentTimeMillis();
// 从tick=1的位置开始执行
tick = 1;
while (!shutdown.get()) {
// 等到下一个tick的时间到达
final long deadline = waitForNextTick();
if (deadline > 0) {
// 获取到期的任务
fetchExpiredTimeouts(expiredTimeouts, deadline);
// 执行到期的任务
notifyExpiredTimeouts(expiredTimeouts);
}
}
}
执行线程会一直等到时间到达当前tick的结束时间.例如之前上面说的K点或Q点位置.
代码语言:javascript复制private long waitForNextTick() {
// 计算第tick格子的结束时刻
long deadline = startTime tickDuration * tick;
for (;;) {
final long currentTime = System.currentTimeMillis();
// 可以等价于startTime tickDuration * tick - currentTime
final long sleepTime = tickDuration * tick - (currentTime - startTime);
// 根据上面的等式,它的意思就是说必须等到时间走到了当前格子的结束时刻才终止.
if (sleepTime <= 0) {
break;
}
try {
// 如果时间还没有走到tick格子的结束时刻则sleep
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
if (shutdown.get()) {
return -1;
}
}
}
// tick加1
tick ;
return deadline;
}
通过图形的方式解释下上面的方法
首先会计算出当前tick的deadline的值,就是图中标注的.如果当前时间(currentTime)还没有走到deadline,那么线程睡眠(deadline-currentTime)这么久,'醒来'之后继续检查.只有当前时间>=deadline的时候,才会跳出循环,开始执行任务.
接下来将当前格子中的所有任务遍历一遍,找出任务的deadline(每个任务在放入时间轮的时候,都会有一个deadline值)比图中deadline小的任务,把它们放入一个集合,然后执行它们.
代码语言:javascript复制// 这个方法都在处理同一个格子里面的任务
private void fetchExpiredTimeouts(
List<HashedWheelTimeout> expiredTimeouts,
ReusableIterator<HashedWheelTimeout> i, long deadline) {
List<HashedWheelTimeout> slipped = null;
i.rewind();
// 每个格子的底层都是通过HashMap存储任务的,而且每个格子都有一个迭代器,用于迭代HashMap中的任务.
while (i.hasNext()) {
HashedWheelTimeout timeout = i.next();
// 有些任务是在这一圈就要执行,有些任务是要等到第2圈,第3圈...才会执行的.这里只会找出这一圈需要执行的任务
if (timeout.remainingRounds <= 0) {
i.remove();
// 如果任务已经过期,则加入集合中,接下来就会执行这个任务
if (timeout.deadline <= deadline) {
expiredTimeouts.add(timeout);
} else {
if (slipped == null) {
slipped = new ArrayList<HashedWheelTimer.HashedWheelTimeout>();
}
// 有些任务可能放错了格子,需要重新放入格子
slipped.add(timeout);
}
} else {
timeout.remainingRounds --;
}
}
if (slipped != null) {
for (HashedWheelTimeout timeout: slipped) {
scheduleTimeout(timeout, timeout.deadline - deadline);
}
}
}
执行任务
代码语言:javascript复制private void notifyExpiredTimeouts(List<HashedWheelTimeout> expiredTimeouts) {
// 遍历列表,执行任务
for (int i = expiredTimeouts.size() - 1; i >= 0; i --) {
expiredTimeouts.get(i).expire();
}
expiredTimeouts.clear();
}
简单做个总结,如下图,时间轮在执行的过程,假如当前正在准备执行tick=2的格子中的任务,如果当前时间没有走到deadline时刻,那么线程睡眠,直到时间到达deadline时刻,那么就开始执行格子中的任务(每个格子中的任务都是外部线程提交到时间轮里的).
首先需要筛选出格子中任务的deadline<=图中的deadline.只有任务的deadline<=图中标记的deadline,才会执行这些满足条件的任务.未满足条件的任务只能等到下一圈,或者下下圈才能被执行,这是根据任务的延时时间决定的.
任务在提交到时间轮的时候,它何时被执行,已经被确定了.
假如正在执行S格子中的任务,这个时候添加了一个任务task,它要在D这个时刻被执行,那么当时间走到N这个deadline点的时候,任务task就一定要被执行.
一个任务要在延时300ms的时候被执行,但是未必一定是在延时300ms后被执行.只有时间轮的时刻走到了类似图中N点这样的位置的时候,才会执行当前格子(也就是当前tick)中比N点时刻小的任务,就是任务的deadline的时刻比N点的时刻小,才会执行这样的任务.