How to Use?
和计划任务线程池ScheduledThreadPoolExecutor的功能类似,HashedWheelTimer也提供了延时执行的功能。前者基于延时队列,而后者基于时间轮实现。先看它的使用方式。
HashedWheelTimer hashedWheelTimer = new HashedWheelTimer(100, TimeUnit.MILLISECONDS);
hashedWheelTimer.newTimeout(timeout -> System.out.println("Task:" + System.currentTimeMillis()), 4, TimeUnit.SECONDS);
概述
时间轮其实就是一种环形的数据结构,可以想象成时钟,分成很多格子,一个格子代表一段时间。并用一个链表保存在该格子上的计划任务,同时一个指针随着时间一格一格转动,并执行相应格子中的到期任务。任务通过时间取模决定放入那个格子。如下图所示:
相关概念:
- tick,时刻即轮上的指针,指向某一个格子
- ticksPerWheel,轮盘一圈包含的格子数,也就是轮盘总刻度数
- tickDuration,时刻间距,也就是指针走完一个格子的时长,值越小,精度越高。
- roundDuration,计时周期,轮盘指针走完一圈耗时,roundDuration=ticksPerWheel∗tickDuration。当任务的延期时长delay超出计时周期时,任务放入对应桶中的同时保存剩余圈数:roundsRemaining=delay / roundDuration
- bucket,相邻刻度之间为桶,桶中以链表或其他形式存放延时任务。当指针走过该桶时,桶中超时的延时任务开始启动
下面我们逐个分析概念对应的实现,最后讲述HashedWheelTimer如何将这些零件组合起来从而实现计划任务的功能。
HashedWheelTimeout
HashedWheelTimeout是一个定时任务的包装类,同意Bucket下的任务会以双向列表连接。
private static final class HashedWheelTimeout implements Timeout {
private static final int ST_INIT = 0;
private static final int ST_CANCELLED = 1;
private static final int ST_EXPIRED = 2;
// Unsafe CAS修改state工具
private static final AtomicIntegerFieldUpdater<HashedWheelTimeout> STATE_UPDATER = AtomicIntegerFieldUpdater.newUpdater(HashedWheelTimeout.class, "state");
// 所属的时间轮
private final HashedWheelTimer timer;
// 计划的任务
private final TimerTask task;
// 执行时间
private final long deadline;
// 任务的状态,默认为初始化0,取消为1,过期为2
private volatile int state = ST_INIT;
// 所属的格子
HashedWheelBucket bucket;
long remainingRounds;
// 前后节点
HashedWheelTimeout prev;
HashedWheelTimeout next;
}
使用者在构造函数传入TimerTask的实现类,构造出一个HashedWheelTimeout实例,TimerTask task属性指向我们传入的实现类。
考虑一种情况任务A在第一个Bucket上,而任务B延时远远超过任务A,很巧B也在第一个Bucket上。那么当执行第一个Bucket时,如何区分任务A执行而任务B不执行呢?
remainingRounds初始值为根据延时的长短计算出一个圈数,每当执行到该Bucket,remainingRounds自减,当它为0时,表示时间到了即可以执行。
当任务只有处于初始化状态时才可以取消我们可以通过cancel的CAS操作看出。
public boolean cancel() {
if (!compareAndSetState(ST_INIT, ST_CANCELLED)) {
return false;
}
timer.cancelledTimeouts.add(this);
return true;
}
并且这里取消任务之后,并没有将此任务删除,而是加入了一个Mpsc队列,至于删除我们在后面再说。当时间轮转到当前bucket时,工作线程执行expire方法
public void expire() {
if (!compareAndSetState(ST_INIT, ST_EXPIRED)) {
return;
}
task.run(this);
// 省略try catch
}
执行任务之前CAS修改状态为ST_EXPIRED,接着执行任务,把自身传入。
HashedWheelBucket
HashedWheelBucket代表了时间轮上的一个格子,它的作用是维护和它关联的任务列表。该类的内部只有两个属性。
private HashedWheelTimeout head;
private HashedWheelTimeout tail;
关于双向列表的添加和删除逻辑就不在这里展开分析了,主要看一下执行任务的代码。
tick到该格子的时候,worker线程会调用这个方法,根据deadline和remainingRounds判断任务是否可以执行。
public void expireTimeouts(long deadline) {
HashedWheelTimeout timeout = head;
// 遍历格子中的所有定时任务
while (timeout != null) {
boolean remove = false;
if (timeout.remainingRounds <= 0) { // 定时任务到期
if (timeout.deadline <= deadline) {
timeout.expire();
} else {// 一般不会进入此逻辑}
remove = true;
} else if (timeout.isCancelled()) {// 已被取消
remove = true;
} else { //没有到期,轮数-1
timeout.remainingRounds --;
}
// 先保存next,因为移除后next将被设置为null
HashedWheelTimeout next = timeout.next;
if (remove) {
remove(timeout);
}
timeout = next;
}
}
Worker
HashedWheelTimer还有个内部类Worker,实现Runnable接口,它是时间轮的核心线程类。tick的转动,任务执行都是在这个线程中处理的。
Worker的状态分为三种类型,并且标识状态的字段声明在HashedWheelTimer类中。
private static final AtomicIntegerFieldUpdater<HashedWheelTimer> WORKER_STATE_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(HashedWheelTimer.class, "workerState");
private final Thread workerThread;// worker对应的线程
public static final int WORKER_STATE_INIT = 0;
public static final int WORKER_STATE_STARTED = 1;
public static final int WORKER_STATE_SHUTDOWN = 2;
private volatile int workerState; // 0 - init, 1 - started, 2 - shut down
// 用于等待worker启动的闭锁
private final CountDownLatch startTimeInitialized = new CountDownLatch(1);
startTimeInitialized作用是保证worker先于添加任务。下面进入worker类。
// 超时任务集合
private final Set<Timeout> unprocessedTimeouts = new HashSet<Timeout>();
// 格子的指针
private long tick;
下面进入核心run方法:
@Override
public void run() {
// 初始化HashedWheelTimer的startTime属性
startTime = System.nanoTime();
// 通知阻塞在 HashedWheelTimer#start的线程结束等待
startTimeInitialized.countDown();
do {
// 等到值下一个格子的deadline
final long deadline = waitForNextTick();
if (deadline > 0) {
int idx = (int) (tick & mask);// 计算数组的下标
processCancelledTasks();// 移除被取消的任务
HashedWheelBucket bucket = wheel[idx];
transferTimeoutsToBuckets();// 把任务句柄从队列 timeouts 中正式加入对应的桶中
bucket.expireTimeouts(deadline); // 过期执行格子中的任务
tick++;
}
// 只要时间轮的状态为WORKER_STATE_STARTED,就循环的“转动”tick,循环判断响应格子中的到期任务
} while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);
// 这里应该是时间轮停止了,清除所有格子中的任务,并加入到未处理任务列表,以供stop()方法返回
for (HashedWheelBucket bucket: wheel) {
bucket.clearTimeouts(unprocessedTimeouts);
}
// 将还没有加入到格子中的待处理定时任务队列中的任务取出,如果是未取消的任务,则加入到未处理任务队列中,以供stop()方法返回
for (;;) {
HashedWheelTimeout timeout = timeouts.poll();
if (timeout == null) {
break;
}
if (!timeout.isCancelled()) {
unprocessedTimeouts.add(timeout);
}
}
// 处理取消的任务
processCancelledTasks();
}
继续分析等待的方法:
private long waitForNextTick() {
// 计算走完当前格子的相对时间
long deadline = tickDuration * (tick + 1);
for (;;) {
final long currentTime = System.nanoTime() - startTime;// 当前先对时间
long sleepTimeMs = (deadline - currentTime + 999999) / 1000000;// 需要等待的时间
// 省略无用代码
try {
Thread.sleep(sleepTimeMs);
} catch (InterruptedException ignored) {
if (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_SHUTDOWN) {
return Long.MIN_VALUE;
}
}
}
}
}
还有一个重要的操作是transferTimeoutsToBuckets方法,因为添加任务是直接加入到private final Queue<HashedWheelTimeout> timeouts = PlatformDependent.newMpscQueue();
中,它是线程安全的无锁队列。每次调用最多只会转移十万个任务。
private void transferTimeoutsToBuckets() {
for (int i = 0; i < 100000; i++) {
HashedWheelTimeout timeout = timeouts.poll();
if (timeout == null) {
break;
}
if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) {
continue;
}
long calculated = timeout.deadline / tickDuration;// 计算所在的格子数
timeout.remainingRounds = (calculated - tick) / wheel.length;// 计算圈数
final long ticks = Math.max(calculated, tick); // 如果过时,加入到当前tick对应的桶中
int stopIndex = (int) (ticks & mask);
HashedWheelBucket bucket = wheel[stopIndex];
bucket.addTimeout(timeout);
}
}
通过上面代码分析我们可以知道每等待到下一个格子的时间结束的点,都会有三步骤
- 移除取消的任务
- 从任务队列中添加最多10w个任务至对应的格子中
- 执行任务
当worker被shutdown了之后也会有三个步骤
- 把所有格子(桶)中的任务移到未处理队列
- 把所有等待队列中的任务添加到未处理队列
- 处理取消的任务
这里有两个队列等待队列和取消队列,所有新任务的添加都会加入到等待队列,每次tick的移动都会从中取任务,某个任务取消会立即被放入取消队列。而在每次tick的移动都会处理取消队列中的任务,也就是将其从bucket的任务列表中移除。
HashedWheelTimer
下面通过构造以及使用的流程来分析HashedWheelTimer。
public HashedWheelTimer(ThreadFactory threadFactory,long tickDuration, TimeUnit unit, int ticksPerWheel, boolean leakDetection,long maxPendingTimeouts) {
wheel = createWheel(ticksPerWheel);//创建轮盘,HashedWheelBucket数组类型
mask = wheel.length - 1;
this.tickDuration = unit.toNanos(tickDuration);// 格子的时长,转化为纳秒
workerThread = threadFactory.newThread(worker); // 创建worker线程
// 是否开启内存泄漏
leak = leakDetection || !workerThread.isDaemon() ? leakDetector.track(this) : null;
// 最大等待任务数
this.maxPendingTimeouts = maxPendingTimeouts;
// 实例数第一次超出64警告
if (INSTANCE_COUNTER.incrementAndGet() > INSTANCE_COUNT_LIMIT && WARNED_TOO_MANY_INSTANCES.compareAndSet(false, true)) {
reportTooManyInstances();
}
}
添加新任务至等待队列
public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
long pendingTimeoutsCount = pendingTimeouts.incrementAndGet(); // 等待任务数自增 1
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();
// 计算相对执行时间
long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;
// 构造延时任务对象
HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
// 添加至等待任务队列
timeouts.add(timeout);
return timeout;
}
public void start() {
switch (WORKER_STATE_UPDATER.get(this)) {
case WORKER_STATE_INIT:
if (WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_INIT, WORKER_STATE_STARTED)) {
workerThread.start();// 启动worker线程
}
break;
case WORKER_STATE_STARTED:
break;
case WORKER_STATE_SHUTDOWN:
throw new IllegalStateException("cannot be started once stopped");
default:
throw new Error("Invalid WorkerState");
}
while (startTime == 0) {
try {
startTimeInitialized.await();// 等待至worker启动
} catch (InterruptedException ignore) {
}
}
}
注意
任务的执行都是通过worker单线程执行,如过时间格子长度设置过小而任务执行时间较长,则会导致执行的时间不精确。因此可以将任务异步的执行可以加速任务的遍历。