定时任务时间轮算法介绍

一.时间轮算法介绍:

可以当做是一个时钟,定时任务是挂载到每个刻度上面的,当随着时间指针不断往前走,走到对应的格子上的时候就会执行对应的定时任务,如果一个刻度上面有多个定时任务,就会形成链表的结构.

定时任务时间轮算法介绍_第1张图片

多级时间轮:

就是类似于时钟的秒,分,时三个刻度,比如一个5分05秒的定时任务需要执行,他的最大刻度是分,首先放在分钟刻度上,首先计算他的分刻度编号,刻度编号 = (过期时间 + 当前时间 )% 总刻度数,假设现在当前时间是0,(5+0)%59=5,那么他就在分的刻度5上,当秒针执行60秒的时候就到了分,当执行到5分的时候,就会找到对应这个5分05秒的定时任务,然后发现他还有5秒这个刻度,就会把5秒这个过期时间在计算到对应的秒的刻度上,然后秒执行到5的时候,就会执行该定时任务

二.时间轮数据结构

是一个循环数组+双向链表的结构,他的每个刻度就是数组中的一个槽,槽用来存在该刻度需要被执行的定时任务。如果同一时刻中是会存在多个定时任务的,所以每个槽中放一个链表或者队列就可以了.

三.为什么需要多层级时间轮

日常开发中,我们执行定时任务有秒级别的,有分钟级别,还是时级别的等等,如果按照秒来切分数组,就会造成执行小时级别的定时任务的时候,造成内存分配过大,所以这里如果分别有时,分,秒级别的时间轮,这样首先会分配到小时时间轮上,当执行到的时候,再把他分配到分钟上,最后分配到秒级时间轮,有最小单位时间轮来执行该定时任务.

四.时间轮的应用

很多定时任务框架例如: quartz,elastic-job等等,包括延迟消息推送,心跳检测等等

在Netty中的一个典型应用场景是判断某个连接是否失效,如果失效(如客户端由于网络原因导致到服务器的心跳无法送达),则服务器会主动断开连接,释放资源

五.netty的时间算法类HashedWheelTimer

    /**
     * @param tickDuration 间隔时间(每个槽的时间)
     * @param unit  间隔时间的单位
     * @param ticksPerWheel 时间轮的大小
     */
    public HashedWheelTimer(long tickDuration, TimeUnit unit, int ticksPerWheel) {
        //这里没有指定线程池,默认创建了一个线程池
        this(Executors.defaultThreadFactory(), tickDuration, unit, ticksPerWheel);
    }

1.构造方法中会新建数组,创建工作线程,该线程会定期的移动指针,扫描链表任务

 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);
        //创建数组
        HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel];
        for (int i = 0; i < wheel.length; i ++) {
           //每个数组槽位置会初始化一个格子对象
            wheel[i] = new HashedWheelBucket();
        }
        return wheel;
    }

2.添加一个定时任务

@Override
    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 + ")");
        }

        //启动工作线程
        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对象
        HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
        //将任务加入timeouts队列
        timeouts.add(timeout);
        return timeout;
    }

3.工作线程启动方法

 @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;
        }

        // 释放start方法中的CountDownLatch锁
        startTimeInitialized.countDown();

        do {
            //waitForNextTick 主要是指针跳动,内部使用Thread.sleep实现
            final long deadline = waitForNextTick();
            if (deadline > 0) {
                //tick和mask进行按位与操作获取到当前数组下标位置
                int idx = (int) (tick & mask);
                //从时间轮中移除所有已经取消的定时任务
                processCancelledTasks();
                HashedWheelTimer.HashedWheelBucket bucket =
                        wheel[idx];
                //将队列中的定时任务放入到时间轮中
                transferTimeoutsToBuckets();
                //遍历链表任务,将达到执行时间的任务触发执行
                bucket.expireTimeouts(deadline);
                tick++;
            }
        } while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);

        //工作线程停止后,将时间轮上的所有任务放入unprocessedTimeouts集合
        for (HashedWheelTimer.HashedWheelBucket bucket: wheel) {
            bucket.clearTimeouts(unprocessedTimeouts);
        }
        //将任务队列中的任务也放入unprocessedTimeouts集合
        for (;;) {
            HashedWheelTimer.HashedWheelTimeout timeout = timeouts.poll();
            if (timeout == null) {
                break;
            }
            if (!timeout.isCancelled()) {
                unprocessedTimeouts.add(timeout);
            }
        }
        //移除所有的未处理的定时任务
        processCancelledTasks();
    }

4.transferTimeoutsToBuckets方法把定时任务放入时间轮中

 private void transferTimeoutsToBuckets() {
        // 每次最多只迁移 10W 个定时任务,主要是为了防止迁移时间过长,导致时间轮中的任务延迟执行
        for (int i = 0; i < 100000; i++) {
            HashedWheelTimer.HashedWheelTimeout timeout = timeouts.poll();
            if (timeout == null) {
                // all processed
                break;
            }
            if (timeout.state() == HashedWheelTimer.HashedWheelTimeout.ST_CANCELLED) {
                // Was cancelled in the meantime.
                continue;
            }

            //计算任务需要放入的数组位置
            long calculated = timeout.deadline / tickDuration;
            timeout.remainingRounds = (calculated - tick) / wheel.length;

            final long ticks = Math.max(calculated, tick); // Ensure we don't schedule for past.
            //按位与获取任务的执行位置
            int stopIndex = (int) (ticks & mask);

            HashedWheelTimer.HashedWheelBucket bucket = wheel[stopIndex];
            //将任务放入当前数组上的链表
            bucket.addTimeout(timeout);
        }
    }

你可能感兴趣的:(java进阶,算法,数据结构)