HashedWheelTimer时间轮定时任务原理分析

一、示例代码

 

HashedWheelTimer时间轮是一个高性能,低消耗的数据结构,它适合用非准实时,延迟的短平快任务,例如心跳检测。

时间轮是一种非常惊艳的数据结构。其在Linux内核中使用广泛,是Linux内核定时器的实现方法和基础之一。Netty内部基于时间轮实现了一个HashedWheelTimer来优化I/O超时的检测,

由于Netty动辄管理100w+的连接,每一个连接都会有很多超时任务。比如发送超时、心跳检测间隔等,如果每一个定时任务都启动一个Timer,不仅低效,而且会消耗大量的资源。
在Netty中的一个典型应用场景是判断某个连接是否idle,如果idle(如客户端由于网络原因导致到服务器的心跳无法送达),则服务器会主动断开连接,释放资源。得益于Netty NIO的优异性能,基于Netty开发的服务器可以维持大量的长连接,单台8核16G的云主机可以同时维持几十万长连接,及时掐掉不活跃的连接就显得尤其重要。

HashedWheelTimer本质是一种类似延迟任务队列的实现,那么它的特点就是上述所说的,适用于对时效性不高的,可快速执行的,大量这样的“小”任务,能够做到高性能,低消耗。
例如:

  • 心跳检测
  • session、请求是否timeout
    业务场景则有:
  • 用户下单后发短信
  • 下单之后15分钟,如果用户不付款就自动取消订单
@Slf4j
public class HashWheelTimerTest {

    int nIndex = 0;

    public void testHashWheelTimer(){
        log.debug(" hash wheel time--> start." );

        HashedWheelTimer hashedWheelTimer = new HashedWheelTimer();
        for (int i = 0; i < 10; i++) {
            hashedWheelTimer.newTimeout(new TimerTask() {
                @Override
                public void run(Timeout timeout) throws Exception {
                    log.debug(" wheel timer run nIndex:{},timeout:{}",nIndex++,timeout.isExpired() );
                }
            },i + 1  ,TimeUnit.SECONDS);
        }
        log.debug(" hash wheel  time--> end." );
    }

    public static void main(String[] args) {
        HashWheelTimerTest hashWheelTimerTest = new HashWheelTimerTest();
        hashWheelTimerTest.testHashWheelTimer();
        try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

二、创建时间轮定时器对象

1.类成员变量分析

tick,时刻即轮上的指针,指向某一个格子
ticksPerWheel,轮盘一圈包含的格子数,也就是轮盘总刻度数
tickDuration,时刻间距,也就是指针走完一个格子的时长,值越小,精度越高。
roundDuration,计时周期,轮盘指针走完一圈耗时,roundDuration=ticksPerWheel∗tickDuration。当任务的延期时长delay超出计时周期时,任务放入对应桶中的同时保存剩余圈数:roundsRemaining=delay / roundDuration
bucket,相邻刻度之间为桶,桶中以链表或其他形式存放延时任务。当指针走过该桶时,桶中超时的延时任务开始启动
 

public class HashedWheelTimer implements Timer {

    private final Worker worker = new Worker();  //任务执行工作线程,实现runnable
    private final Thread workerThread; //工作线程对象。
    private final long tickDuration; //每个时钟小格子的时间长度。
    private final HashedWheelBucket[] wheel; //时间轮子数组,如果时钟有512个格子,则这个值的长度为512
    private final int mask;  //时间轮数组长度-1,用于计算任务位于哪个格子,进行掩码。
    private final CountDownLatch startTimeInitialized = new CountDownLatch(1);
    private final Queue<HashedWheelTimeout> timeouts = PlatformDependent.newMpscQueue();
// 待执行的延时任务。
    private final long maxPendingTimeouts;

    private volatile long startTime; //工作线程开始时间。


 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;

        long duration = unit.toNanos(tickDuration);
         this.tickDuration = duration;
        workerThread = threadFactory.newThread(worker);
        this.maxPendingTimeouts = maxPendingTimeouts;
    }

2.添加任务分析, 这里就是把task加入到Queue<HashedWheelTimeout> timeouts这个待执行的延迟任务队列中。

  public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
        long pendingTimeoutsCount = pendingTimeouts.incrementAndGet();

        start();
        long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;
        HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
        timeouts.add(timeout);
        return timeout;
    }

 三、定时执行任务。

    1.由workThread中的worker这个对象来负责执行。

  1. 时间轮运行的时候首先会记录一下启动时间(startTime),然后调用startTimeInitialized释放外层的等待线程;
  2. 进入dowhile循环,调用waitForNextTick睡眠等待到下一次的tick指针的跳动,并返回当前时间减去startTime作为deadline
  3. 由于mask= wheel.length -1 ,wheel是2的次方数,所以可以直接用tick & mask 计算出此次在wheel中的槽位
  4. 调用processCancelledTasks将cancelledTimeouts队列中的任务取出来,并将当前的任务从时间轮中移除
  5. 调用transferTimeoutsToBuckets方法将timeouts队列中缓存的数据取出加入到时间轮中
  6. 运行目前指针指向的槽位中的bucket链表数据
  private final class Worker implements Runnable {
        private final Set<Timeout> unprocessedTimeouts = new HashSet<Timeout>();

        private long tick;

        @Override
        public void run() {
            // Initialize the startTime.
            startTime = System.nanoTime();
            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);
        }

2.时间轮指针跳动

 private long waitForNextTick() {
        //计算下一个tick的时间,
        long deadline = tickDuration * (tick + 1);
        for (;;) {
            final long currentTime = System.nanoTime() - startTime;
            //根据当前计算需要sleep的时间。这里加了999999是因为向上取整了1毫秒,假如距离下一个tick的时间为2000010纳秒,那如果sleep 2毫秒是不够的,所以需要多sleep 1毫秒。
            long sleepTimeMs = (deadline - currentTime + 999999) / 1000000;

            //sleepTimeMs <=0 说明下一个tick的时间到了,说明上一个tick执行的时间“太久”了,所以直接返回就好了,不需要sleep
            if (sleepTimeMs <= 0) {
                //currentTime == Long.MIN_VALUE 这个判断不是很理解
                if (currentTime == Long.MIN_VALUE) {
                    return -Long.MAX_VALUE;
                } else {
                    return currentTime;
                }
            }

            //直接sleep等待
            try {
                Thread.sleep(sleepTimeMs);
            } catch (InterruptedException ignored) {
                //Worker被中断,如果是关闭了则返回负数,表示不会执行下一个tick
                if (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_SHUTDOWN) {
                    return Long.MIN_VALUE;
                }
            }
        }
    }

3.转移任务到时间轮中

在调用时间轮的方法加入任务的时候并没有直接加入到时间轮中,而是缓存到了timeouts队列中,所以在运行的时候需要将timeouts队列中的任务转移到时间轮数据的链表中

private void transferTimeoutsToBuckets() {
        //最多取队列的100000的元素出来
        for (int i = 0; i < 100000; i++) {
            HashedWheelTimeout timeout = timeouts.poll();
            if (timeout == null) {
                // all processed
                break;
            }
            //如果timeout被取消了则不做处理
            if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) {
                // Was cancelled in the meantime.
                continue;
            }
            //计算位于实践论的层数
            long calculated = timeout.deadline / tickDuration;
            timeout.remainingRounds = (calculated - tick) / wheel.length;
            //就是timeout已经到期了,也不能放到之前的tick中
            final long ticks = Math.max(calculated, tick); // Ensure we don't schedule for past.
            //计算所在bucket下标,并放进去
            int stopIndex = (int) (ticks & mask);

            HashedWheelBucket bucket = wheel[stopIndex];
            //又是类似链表插入节点的操作
            bucket.addTimeout(timeout);
        }
    }

在这个转移方法中,写死了一个循环,每次都只转移10万个任务。

然后根据HashedWheelTimeout的deadline延迟时间计算出时间轮需要运行多少次才能运行当前的任务,如果当前的任务延迟时间大于时间轮跑一圈所需要的时间,那么就计算需要跑几圈才能到这个任务运行。

最后计算出该任务在时间轮中的槽位,添加到时间轮的链表中。

4.运行时间轮中的任务

当指针跳到时间轮的槽位的时间,会将槽位的HashedWheelBucket取出来,然后遍历链表,运行其中到期的任务。

public void expireTimeouts(long deadline) {
        HashedWheelTimeout timeout = head;
        //把bucket的所有timeout取出来执行
        while (timeout != null) {
            HashedWheelTimeout next = timeout.next;
            if (timeout.remainingRounds <= 0) {
                next = remove(timeout);
                if (timeout.deadline <= deadline) {
                    //timeout的真正执行
                    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));
                }
            //该timeout被取消了则移除掉    
            } else if (timeout.isCancelled()) {
                next = remove(timeout);
            //否则层数减一,等待下一轮的到来
            } else {
                timeout.remainingRounds --;
            }
            timeout = next;
        }
    }

HashedWheelBucket是一个链表,所以我们需要从head节点往下进行遍历。如果链表没有遍历到链表尾部那么就继续往下遍历。

获取的timeout节点节点,如果剩余轮数remainingRounds大于0,那么就说明要到下一圈才能运行,所以将剩余轮数减一;

如果当前剩余轮数小于等于零了,那么就将当前节点从bucket链表中移除,并判断一下当前的时间是否大于timeout的延迟时间,如果是则调用timeout的expire执行任务。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Netty是一个基于Java的高性能网络通信框架,它提供了一些方便的功能,包括时间定时任务时间是一种用于执行定时任务数据结构,它可以提高定时任务的触发精度和执行效率。 在Netty中,时间定时任务是通过`HashedWheelTimer`类实现的。下面是一个简单的示例代码,演示如何在Netty中使用时间定时任务: ```java import io.netty.util.HashedWheelTimer; import io.netty.util.Timeout; import io.netty.util.TimerTask; public class TimeWheelExample { public static void main(String[] args) { // 创建时间定时器 HashedWheelTimer timer = new HashedWheelTimer(); // 创建定时任务 TimerTask task = new TimerTask() { @Override public void run(Timeout timeout) throws Exception { System.out.println("定时任务执行"); } }; // 将定时任务提交给时间定时器,延迟2秒后执行 timer.newTimeout(task, 2, TimeUnit.SECONDS); } } ``` 在上面的示例中,我们首先创建了一个`HashedWheelTimer`实例,然后创建了一个`TimerTask`对象,定义了要执行的定时任务。最后,我们使用`timer.newTimeout()`方法将定时任务提交给时间定时器,并指定了延迟时间为2秒。 当时间定时器触发定时任务时,会调用`run()`方法执行任务。在这个例子中,定时任务执行时,会简单地打印一条消息。 需要注意的是,时间定时任务仅限于在Netty中使用,如果你想在其他环境或框架中使用时间定时任务,可能需要使用其他的定时任务实现方式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值