Netty之HashedWheelTimer源码分析

简介
HashedWheelTimer是Netty的一个工具类,来自netty-common包,简单来说用于实现延时任务。
使用场景:dubbo失败重试、netty中长连接超时(如客户端由于网络原因导致无法传送心跳至服务端)。

优缺点
优点:添加、删除、取消延迟任务,能高效地处理大批定时任务。
缺点:占用内存较高、避免创建多个HashedWheelTimer实例,时间精度要求不高。

结构图

先根据上图来绍一下HashedWheelTimer的一些基本概念
workerThread用于处理延迟定时任务的线程,HashedWheelTimer是单线程处理的。它会在每次tick执行一个bucket中的定时任务以及其它操作,定时任务不能有较大的耗时,否则会导致定时任务执行的准时和有效性。
wheel表示一个时间轮,它是一个环形数组,数组上的每个元素类似Map结构对象,每个元素对应着一个双向链表,用于存储定时任务。
tick工作线程周期性地跳动,每一次tick执行对应bucket中的任务。
hash在时间轮上的hash函数。默认是tick%bucket的数量,即将某个时间节点映射到时间轮上的某个格子上。源码中用了&位运算来代替了求模%,提高执行效率。
bucket时间轮上的一个格子,它维护的是一个Timeout的双向链表,保存的是这个哈希映射到这个格子上的所有Timeout任务。
timeout代表一个定时任务,其中记录了自己的deadline,运行逻辑以及在bucket中需要驻留的轮数,比如1s和11s,长度10,则它们对应的timeout中轮数就应该是0和1。这样当遍历一个bucket中所有的timeout时,只要轮数为0就会被执行,其它情况轮数减1。

接口概览
在分析源码之前,先大致了解下它的接口定义及相关类,从下图我们可以看出Timer、Timeout以及TimerTask之间的大致关系。

HashedWheelTimer是接口io.netty.util.Timer的实现,而Timer类中只有两个方法
Timeout持有上层Timer实例和下层TimeTask实例以及取消方法
TimeTask只有一个方法

public interface Timer {
    // 创建一个定时任务
    Timeout newTimeout(TimerTask task, long delay, TimeUnit unit);
    // 停止所有的还没有被执行的定时任务
    Set<Timeout> stop();
}

public interface TimerTask {
    void run(Timeout timeout) throws Exception;
}

public interface Timeout {
    // 上层Timer实例
    Timer timer();
    // 下层TimerTask实例
    TimerTask task();
    boolean isExpired();
    boolean isCancelled();
    boolean cancel();
}

HashedWheelTimer源码
上面都是为分析源码所做的一些准备,有助于对源码的阅读,下面步入正题,进行HashedWheelTimer的源码解读。

    public HashedWheelTimer(
            ThreadFactory threadFactory,
            long tickDuration, TimeUnit unit, int ticksPerWheel, boolean leakDetection,
            long maxPendingTimeouts) {
        // 线程工厂,用于创建worker线程
        if (threadFactory == null) {
            throw new NullPointerException("threadFactory");
        }
        // tick的时间单位
        if (unit == null) {
            throw new NullPointerException("unit");
        }
        // tick的时间间隔
        if (tickDuration <= 0) {
            throw new IllegalArgumentException("tickDuration must be greater than 0: " + tickDuration);
        }
        // 时间轮上一轮有多少个tick/bucket
        if (ticksPerWheel <= 0) {
            throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel);
        }
        // 将时间轮的大小规范化到2的n次方,这样可以用位运算来处理mod操作,提高效率
        wheel = createWheel(ticksPerWheel);
        // 计算位运算需要的掩码
        mask = wheel.length - 1;
        // 转换时间间隔到纳秒
        long duration = unit.toNanos(tickDuration);
        // 防止溢出
        if (duration >= Long.MAX_VALUE / wheel.length) {
            throw new IllegalArgumentException(String.format(
                    "tickDuration: %d (expected: 0 < tickDuration in nanos < %d",
                    tickDuration, Long.MAX_VALUE / wheel.length));
        }
        // 时间间隔至少要1ms
        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线程
        workerThread = threadFactory.newThread(worker);
        // 处理泄露监控(本篇文章不涉及)
        leak = leakDetection || !workerThread.isDaemon() ? leakDetector.track(this) : null;
        // 设置最大等待任务数
        this.maxPendingTimeouts = maxPendingTimeouts;
        // 限制timer实例数,避免过多的timer线程导致内存泄露,反而影响性能
        if (INSTANCE_COUNTER.incrementAndGet() > INSTANCE_COUNT_LIMIT &&
            WARNED_TOO_MANY_INSTANCES.compareAndSet(false, true)) {
            reportTooManyInstances();
        }
    }


private static HashedWheelBucket[] createWheel(int ticksPerWheel) {
        // 时间轮格子数大小限制
        if (ticksPerWheel <= 0) {
            throw new IllegalArgumentException(
                    "ticksPerWheel must be greater than 0: " + ticksPerWheel);
        }
        if (ticksPerWheel > 1073741824) {
            throw new IllegalArgumentException(
                    "ticksPerWheel may not be greater than 2^30: " + ticksPerWheel);
        }
        // 计算时间轮格子数
        ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel);
        // 创建bucket数组并填充
        HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel];
        for (int i = 0; i < wheel.length; i ++) {
            wheel[i] = new HashedWheelBucket();
        }
        return wheel;
    }

    private static int normalizeTicksPerWheel(int ticksPerWheel) {
        int normalizedTicksPerWheel = 1;
        // 这里要求时间轮格子数达到 >= 传入ticksPerWheel的最小数,并且是2的n次方
        while (normalizedTicksPerWheel < ticksPerWheel) {
            normalizedTicksPerWheel <<= 1;
        }
        return normalizedTicksPerWheel;
    }

初始化的HashedWheelBucket数组的长度必须是2的幂次方。HashedWheelTimer初始化完成后,又如何向时间轮里添加定时任务,这就要看newTimeOut()这个方法了!

public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
        if (task == null) {
            throw new NullPointerException("task");
        }
        if (unit == null) {
            throw new NullPointerException("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 + ")");
        }
        // 启动work线程
        start();
        // 计算定时任务剩余执行时间,它是一个相对时间:当前时间+延迟时间-Timer启动时间
        long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;

        // System.nanoTime()返回值不确定:正、负、0都有可能
        // deadline超出long范围最大值,则设置Long.MAX_VALUE
        if (delay > 0 && deadline < 0) {
            deadline = Long.MAX_VALUE;
        }
        // 将定时任务封装成HashedWheelTimeout
        HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
        // 将定时任务存储到timeouts队列,注意是队列
        timeouts.add(timeout);
        return timeout;
    }

上面方法中提到start方法,它会启动时间轮轮询的线程即workThread。

public void start() {
        // 判断HashedWheelTimer状态,如果状态开启,则开启轮询线程
        switch (WORKER_STATE_UPDATER.get(this)) {
            case WORKER_STATE_INIT:
                if (WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_INIT, WORKER_STATE_STARTED)) {
                    workerThread.start();
                }
                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 {
                // 阻塞当前线程,目的是保证轮询线程workerThread开启
                startTimeInitialized.await();
            } catch (InterruptedException ignore) {
                // Ignore - it will be ready very soon.
            }
        }
    }

既然工作起线workerThread.start()启动了, 那么它又是怎么来处理任务的呢?

public void run() {
        // 在HashedWheelTimer中,用的都是相对时间,所以需要启动时间作为基准,并且要用volatile修饰
        startTime = System.nanoTime();
        // 此处如果是0置1目的:保证start方法while跳出循环,因为System.nanoTime()有可能返回0,如果不置1,start方法while进行无限循环
        if (startTime == 0) {
            startTime = 1;
        }
        // 第一个提交任务的线程正await呢,唤醒它
        startTimeInitialized.countDown();
        // 接下来这个do-while是真正开始执行任务
        do {
            // 根据周期时间tickDuration,进行周期性的tick下一个槽位
            final long deadline = waitForNextTick();
            if (deadline > 0) {
                // 该次tick,bucket数组对应的index
                int idx = (int) (tick & mask);

                // 处理已经取消的任务
                processCancelledTasks();
                HashedWheelBucket bucket = wheel[idx];

                // 将队列中的任务转移到相应的buckets中
                transferTimeoutsToBuckets();
                // 执行进入到这个bucket中的任务
                bucket.expireTimeouts(deadline);
                tick++;
            }
        } while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);

        /* 到这里,说明这个timer要关闭了,做一些清理工作 */
        // 将所有bucket中没有执行的任务,添加到unprocessedTimeouts这个HashSet中,
        // 主要目的是用于stop()方法返回
        for (HashedWheelBucket bucket: wheel) {
            bucket.clearTimeouts(unprocessedTimeouts);
        }
        // 将任务队列中的任务也添加到unprocessedTimeouts中
        for (;;) {
            HashedWheelTimeout timeout = timeouts.poll();
            if (timeout == null) {
                break;
            }
            if (!timeout.isCancelled()) {
                unprocessedTimeouts.add(timeout);
            }
        }
        processCancelledTasks();
    }

在上面方法中,轮询时间轮执行对应槽位的定时任务,在执行之前会先将存储在队列中的任务按照各自的时间放入对应的槽位中,接下来咱们来看如何根据周期时间进行tick。

private long waitForNextTick() {
            // 获取下一个槽位的等待时间
            long deadline = tickDuration * (tick + 1);

            for (; ; ) {
                // 获取当前时间间隔
                final long currentTime = System.nanoTime() - startTime;
                // 计算tick到下一个槽位需要等待的时间
                long sleepTimeMs = (deadline - currentTime + 999999) / 1000000;

                // 当前时间间隔大于等于下一个槽位周期时间,不需要等待,直接返回(从这个地方就可以得出HashedWheelTimer对时间精度要求不高,并不是严格按照延迟时间来执行的)
                if (sleepTimeMs <= 0) {
                    if (currentTime == Long.MIN_VALUE) {
                        return -Long.MAX_VALUE;
                    } else {
                        return currentTime;
                    }
                }
                if (isWindows()) { // OS相关
                    sleepTimeMs = sleepTimeMs / 10 * 10;
                }

                try {
                    // 当前时间间隔小于下一个槽位周期时间,则进行休眠
                    Thread.sleep(sleepTimeMs);
                } catch (InterruptedException ignored) {
                    if (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_SHUTDOWN) {
                        return Long.MIN_VALUE;
                    }
                }
            }
        }

long sleepTimeMs = (deadline - currentTime + 999999) / 1000000 这一行代码为何+999999?
举个例子,如果deadline - currentTime=1000001,结果是1,+999999结果为2,向上取整。
为了便于理解,下面了解下老版本实现方式,这种实现方式还是一目了然的。

​​​private void waitForNextTick(){
    for(::){
       final long currentTime = System.nanoTime();
       final long sleepTime = tickDuration * tick - (currentTime - startTime);
       
       if(sleepTime <= 0){
          break;
       }
       
       try{
          Thread.sleep(sleepTime / 1000000,(int)(sleepTime % 1000000));
       } catch (InterruptedException e){
          // FIXME: must exit the loop if necessary
       }
    }
}

public static void sleep(long millis, int nanos)
    throws InterruptedException {
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }
        // 1.也就是当nanos大于等于500微秒时,millis就加1.当nanos小于500微秒时,不改变millis的值. 
        // 2.当millis的值为0时,只要nanos不为0,就将millis设置为1.
        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }
        sleep(millis);
    }

分析了如何实现时间间隔轮询,接下来分析如何将任务存储到HashedWheelBucket中?

private void processCancelledTasks() {
            // 处理过期任务
            for (; ; ) {
                HashedWheelTimeout timeout = cancelledTimeouts.poll();
                if (timeout == null) {
                    break;
                }
                try {
                    timeout.remove();
                } catch (Throwable t) {
                }
            }
        }

private void transferTimeoutsToBuckets() {
            // 遍历timeouts队列,默认遍历100000个任务,防止work线程在此处逗留太久
            for (int i = 0; i < 100000; i++) {
                HashedWheelTimeout timeout = timeouts.poll();
                if (timeout == null) {
                    // 队列中没有任务了。说明处理完了
                    break;
                }
                if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) {
                    // 未放入bucket就被取消了
                    continue;
                }

                // calculated表示任务要经过多少个tick
                long calculated = timeout.deadline / tickDuration;
                // 设置任务要经过的轮数
                timeout.remainingRounds = (calculated - tick) / wheel.length;
 
                // 如果任务在timeouts队列里面放久了, 以至于已经过了执行时间,
                // 这个时候就使用当前tick, 也就是放到当前bucket, 这样在方法调用完后就会执行.
                final long ticks = Math.max(calculated, tick);
                int stopIndex = (int) (ticks & mask);
 
                HashedWheelBucket bucket = wheel[stopIndex];
                bucket.addTimeout(timeout); // 加到双向链表末尾
            }
        }

接下来是执行符合条件运行的定时任务,遍历bucket中的整个链表。
注意:Worker线程是按照bucket顺序处理的,所以即使有些任务执行时间超过了100ms,耗费了之后好几个bucket的处理时间,也没关系,这些任务并不会被漏掉。但是有可能被延迟执行,毕竟工作线程是单线程。

// 当tick到该wheel的时候, Worker会调用这个方法, 根据deadline来判断任务是否过期(remainingRounds是否为0)
// 任务到期就执行, 没到期timeout.remainingRounds--, 因为走到这里, 表示改wheel里的任务又过了一轮了.
void expireTimeouts(long deadline) {
            // 获取链表表头任务
            HashedWheelTimeout timeout = head;

            // process all timeouts
            while (timeout != null) {
                // 获取表头的下一个任务
                HashedWheelTimeout next = timeout.next;
                if (timeout.remainingRounds <= 0) {
                    // 将要执行的任务从链表中删除
                    next = remove(timeout);
                    // 任务的时间小于间隔时间,执行任务
                    if (timeout.deadline <= deadline) {
                        // 执行任务
                        timeout.expire();
                    } else {
                        // The timeout was placed into a wrong slot. This should never happen.
                        throw new IllegalStateException(String.format(
                                "timeout.deadline (%d) > deadline (%d)", timeout.deadline, deadline));
                    }
                } else if (timeout.isCancelled()) {
                    next = remove(timeout);
                } else {
                    timeout.remainingRounds--;
                }
                timeout = next;
            }
        }

 public void expire() {
            if (!compareAndSetState(ST_INIT, ST_EXPIRED)) {
                return;
            }
            try {
                // **这个地方就是真正执行封装的task任务,执行具体的任务逻辑**
                task.run(this);
            } catch (Throwable t) {
                if (logger.isWarnEnabled()) {
                    logger.warn("An exception was thrown by " + TimerTask.class.getSimpleName() + '.', t);
                }
            }
        }

最后讲讲Timer时间轮的停止stop方法

 @Override
    public Set<Timeout> stop() {
        // 工作线程不能调用stop
        if (Thread.currentThread() == workerThread) {
            throw new IllegalStateException(
                    HashedWheelTimer.class.getSimpleName() +
                            ".stop() cannot be called from " +
                            TimerTask.class.getSimpleName());
        }

        if (!WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_STARTED, WORKER_STATE_SHUTDOWN)) { 
            // workerState can be 0 or 2 at this moment - let it always be 2.
            if (WORKER_STATE_UPDATER.getAndSet(this, WORKER_STATE_SHUTDOWN) != WORKER_STATE_SHUTDOWN) {
                INSTANCE_COUNTER.decrementAndGet();
                if (leak != null) {
                    boolean closed = leak.close(this);
                    assert closed;
                }
            }

            return Collections.emptySet();
        }

        try {
            boolean interrupted = false;
            while (workerThread.isAlive()) {
                workerThread.interrupt();
                try {
                    workerThread.join(100);
                } catch (InterruptedException ignored) {
                    interrupted = true;
                }
            }

            if (interrupted) {
                Thread.currentThread().interrupt();
            }
        } finally {
            INSTANCE_COUNTER.decrementAndGet();
            if (leak != null) {
                boolean closed = leak.close(this);
                assert closed;
            }
        }
        // 未被执行的任务
        return worker.unprocessedTimeouts();
    }

总结
HashWheelTimer主要是基于时间轮的算法,添加的任务封装成timeout对象存放到任务队列中,时间轮的格子用bucket数组表示,bucket是一个双向链表,存储timeout对象,内部启动一个work线程自旋到每个tick时都sleep到对应的时间,然后将timeout任务队列里面的任务放到对应的格子里面,计算圈数,之后轮询当前tick对应的格子里面的链表,圈数为0的timeout任务执行掉,其余圈数减一,等待下个tick触发。

整个时间轮的调度都是在一个worker线程里面完成,对于耗时较大的定时任务,如果直接扔进去处理显然会影响其它任务的正常执行。因此时间轮主要的缺点在于时间精确性不能过于保证。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值