Netty 工具类 —— HashedWheelTimer 讲解

时间:2021-12-23 01:31:41

一、前言

在网络通信中管理上万的连接,每个连接都有超时任务,如果为每个任务启动一个TImer超时器,那么会占用大量资源。为了解决这个问题,可用Netty工具类HashedWheelTimer。

二、HashedWheelTimer原理

1.概论

(学习一个类,最好的方式是看api文档或源码的注释,我下载了Netty源码)

这个类用来计划执行非精准的I/O超时。可以通过指定每一格的时间间隔来改变执行时间的精确度。在大多数网络应用中,I/O超时不需要十分准确,因此,默认的时间间隔是100 毫秒,这个值适用于大多数场合。HashedWheelTimer内部结构可以看做是个车轮,简单来说,就是TimerTask的hashTable的车轮。车轮的size默认是512,可以通过构造函数自己设置这个值。注意,当HashedWheelTimer被实例化启动后,会创建一个新的线程,因此,你的项目里应该只创建它的唯一一个实例。

(这个类的源自一位教授的论文 Tony Lauck's paper。算法是代码的灵魂,最难的是算法)

2.结构

下方图示阐述了大致的结构模型

Netty 工具类 —— HashedWheelTimer 讲解

Demo

 @Test(timeout = 50000000)
public void testTimerOverflowWheelLength() throws InterruptedException {
final HashedWheelTimer timer = new HashedWheelTimer(
Executors.defaultThreadFactory(), , TimeUnit.MILLISECONDS, ); timer.newTimeout(new TimerTask() {
@Override
public void run(final Timeout timeout) throws Exception {
System.out.println("lee"); //打印名字
}
}, , TimeUnit.MILLISECONDS); Thread.sleep(10000);
assertFalse(timer.stop().isEmpty());
}

通过上面demo,我再说明一下hashedWheelTimer类的构造。

超时任务1000毫秒,超时之后,由hashedWheelTimer类中的worker线程,执行超时之后的任务(打印名字)。hashedWheelTimer有32个槽(相当于一个圆的32分之一),每移动一个槽的时间是100毫秒。
任务需要经过的tick数为: 1000 / 100 = 10次         (等待时长 / tickDuration)
任务需要经过的轮数为  : 10次 / 32次/轮 = 0轮     (tick总次数 / ticksPerWheel)
因为任务超时后不能马上被worker线程执行,需要等到worker线程移到相应卡槽位置时才会执行,因此说执行时间不精确。

hashedWheelTimer的核心是Worker线程,主要负责每过tickDuration时间就累加一次tick. 同时, 也负责执行到期的timeout任务, 此外,还负责添加timeou任务到指定的wheel中。

接下看看源码部分。

构造器

构造器的各个参数的说明和注释十分详细,应该没有不好理解的地方。

     /**
* Creates a new timer.
*
* @param threadFactory a {@link ThreadFactory} that creates a
* background {@link Thread} which is dedicated to 用来创建后台线程
* {@link TimerTask} execution.
* @param tickDuration the duration between tick 每格时间间隔 默认 100
* @param unit the time unit of the {@code tickDuration} 时间单位 默认 毫秒
* @param ticksPerWheel the size of the wheel *size(一圈多少格)
默认 512 如果不是2的N次方,则去大于该参数的第一个2的N次方
理由 便于Hash值的计算 * @param leakDetection {@code true} if leak detection should be enabled always,
* if false it will only be enabled if the worker thread is not
* a daemon thread.
* @param maxPendingTimeouts The maximum number of pending timeouts after which call to
* {@code newTimeout} will result in
* {@link java.util.concurrent.RejectedExecutionException}
* being thrown. No maximum pending timeouts limit is assumed if
* this value is 0 or negative. 最大待定超时时间 默认 不设置
* @throws NullPointerException if either of {@code threadFactory} and {@code unit} is {@code null}
* @throws IllegalArgumentException if either of {@code tickDuration} and {@code ticksPerWheel} is <= 0
*/
public HashedWheelTimer(
ThreadFactory threadFactory,
long tickDuration, TimeUnit unit, int ticksPerWheel, boolean leakDetection,
long maxPendingTimeouts) {
...

// Normalize ticksPerWheel to power of two and initialize the wheel.
wheel = createWheel(ticksPerWheel);
mask = wheel.length - 1; // Convert tickDuration to nanos.
long duration = unit.toNanos(tickDuration);
...
if (duration < MILLISECOND_NANOS) {
if (logger.isWarnEnabled()) {
logger.warn("Configured tickDuration %d smaller then %d, using 1ms.",
tickDuration, MILLISECOND_NANOS);
}
this.tickDuration = MILLISECOND_NANOS;
} else {
this.tickDuration = duration;
}
// 这里创建worker线程,worker线程是重点。
workerThread = threadFactory.newThread(worker); leak = leakDetection || !workerThread.isDaemon() ? leakDetector.track(this) : null; this.maxPendingTimeouts = maxPendingTimeouts; if (INSTANCE_COUNTER.incrementAndGet() > INSTANCE_COUNT_LIMIT &&
WARNED_TOO_MANY_INSTANCES.compareAndSet(false, true)) {
reportTooManyInstances();
}
}

我们接下来看newTimeout()方法

     @Override
public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
...
long pendingTimeoutsCount = pendingTimeouts.incrementAndGet(); 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 + ")");
} start(); // Add the timeout to the timeout queue which will be processed on the next tick.
// During processing all the queued HashedWheelTimeouts will be added to the correct HashedWheelBucket.
long deadline = System.nanoTime() + unit.toNanos(delay) - startTime; // Guard against overflow.
if (delay > 0 && deadline < 0) {
deadline = Long.MAX_VALUE;
}
HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
timeouts.add(timeout);
return timeout;
}

我们接着进到worker线程的启动:

 /**
* Starts the background thread explicitly. The background thread will
* start automatically on demand even if you did not call this method.
*
* @throws IllegalStateException if this timer has been
* {@linkplain #stop() stopped} already
*/
public void start() { // 这个方法为什么是public的?我们可以在实例化HashedWheelTimer后,主动调用这个方法,启动worker线程
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");
} // Wait until the startTime is initialized by the worker.
// 为了等待worker线程初始化startTime
while (startTime == 0) {
try {
startTimeInitialized.await();
} catch (InterruptedException ignore) {
// Ignore - it will be ready very soon.
}
}
}

下一步,我们看看worker线程里面的操作。

首先是初始化startTime,CountDownLatch的触发,后面do while操作可以看作是圆盘在一个个转动,每转一个就会用worker线程处理,格子中超时的任务。

         @Override
public void run() {
// Initialize the startTime.
startTime = System.nanoTime();
if (startTime == 0) {
// We use 0 as an indicator for the uninitialized value here, so make sure it's not 0 when initialized.
startTime = 1;
} // Notify the other threads waiting for the initialization at start().
startTimeInitialized.countDown(); do {
final long deadline = waitForNextTick();
if (deadline > 0) {
int idx = (int) (tick & mask);
processCancelledTasks();
HashedWheelBucket bucket =
wheel[idx];
transferTimeoutsToBuckets();
bucket.expireTimeouts(deadline);
tick++;
}
} while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED); // Fill the unprocessedTimeouts so we can return them from stop() method.
for (HashedWheelBucket bucket: wheel) {
bucket.clearTimeouts(unprocessedTimeouts);
}
for (;;) {
HashedWheelTimeout timeout = timeouts.poll();
if (timeout == null) {
break;
}
if (!timeout.isCancelled()) {
unprocessedTimeouts.add(timeout);
}
}
processCancelledTasks();
}

----------------------------------------------------------------------------------------------

参考资源:

https://www.jianshu.com/p/328f22432638

https://my.oschina.net/haogrgr/blog/489320

https://chuansongme.com/n/1650380646616