浅谈时间轮算法续集

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

HashedWheelTimer的结构

  • Queue<HashedWheelTimeout> timeouts 提交任务的队列(生产者消费者模式).
  • Thread workerThread 执行任务线程
  • Worker worker首先了Runnable,被workerThread所执行
  • startTime 代表实时间轮转动的开始时间
  • HashedWheelBucket[] wheel存储时间轮的数组
  • HashedWheelBucket为一个双向链表,存储该时间刻度的具体任务
  • mask为wheel数组长度减一,用于延迟任务进行位置定位的关键属性.

image.png

startTime是如何初始化的.
  • CountDownLatch startTimeInitialized = new CountDownLatch(1); 用于时间初始化

startTime是由workerThread一步初始化,并通过CountDownLatch进行唤醒等待的线程;提交任务执行newTimeout(),再该方法中会调用start()方法进行workerThread线程启动,并等待期初始化完成.

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();
            }
            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.
    while (startTime == 0) {
        try {
            startTimeInitialized.await();
        } catch (InterruptedException ignore) {
            // Ignore - it will be ready very soon.
        }
    }
}

再WorkThread线程中会执行Work的run方法初始化时间.

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();
        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;
        }
   }

任务提交

当我们提交一个延迟任务时,会计算一个相对于startTime的相对执行时间.此处的时间统一以纳秒进行计算.

long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;

任务添加到队列中.

HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
timeouts.add(timeout);
HashedWheelTimeout任务状态
  1. ST_INIT 初始化 默认状态
  2. ST_CANCELLED 取消状态
  3. ST_EXPIRED 已过时状态

只有当任务状态ST_INIT时,手动调用cancel(),任务变为ST_CANCELLED; 只有当任务状态ST_INIT时,手动调用expire,任务变为ST_EXPIRED,任务执行后,也会变为ST_EXPIRED. 上述方法操作,都是基于CAS来保证原子性.

if (!compareAndSetState(ST_INIT, ST_EXPIRED)) {
    return;
}
public boolean compareAndSetState(int expected, int state) {
    return STATE_UPDATER.compareAndSet(this, expected, state);
}

计算休眠时间waitForNextTick

首先根据时间刻度计算摆动到下一个刻度的相对(相对于startTime)时间deadline(纳秒), 换算当前时间与startTime的相对时间currentTime.由于Thread.sleep()的时间为毫秒,所以需要将时间换算为毫秒(10^6),此处加999999的原因是tick在上一轮循环执行完后就自增过了.

private long waitForNextTick() {
    long deadline = tickDuration * (tick + 1);
    for (;;) {
        final long currentTime = System.nanoTime() - startTime;
        long sleepTimeMs = (deadline - currentTime + 999999) / 1000000;

        if (sleepTimeMs <= 0) {
            if (currentTime == Long.MIN_VALUE) {
                return -Long.MAX_VALUE;
            } else {
                return currentTime;
            }
        }
        if (PlatformDependent.isWindows()) {
            sleepTimeMs = sleepTimeMs / 10 * 10;
            if (sleepTimeMs == 0) {
                sleepTimeMs = 1;
            }
        }

        try {
            Thread.sleep(sleepTimeMs);
        } catch (InterruptedException ignored) {
            if (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_SHUTDOWN) {
                return Long.MIN_VALUE;
            }
        }
    }
}

延迟任务计算存储位置

private void transferTimeoutsToBuckets() {
    // transfer only max. 100000 timeouts per tick to prevent a thread to stale the workerThread when it just
    // adds new timeouts in a loop.
    for (int i = 0; i < 100000; i++) {
        HashedWheelTimeout timeout = timeouts.poll();
        if (timeout == null) {
            // all processed
            break;
        }
        if (timeout.state() == 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);

        HashedWheelBucket bucket = wheel[stopIndex];
        bucket.addTimeout(timeout);
    }
}
  1. 每次最多取出100k的任务.
  2. 根据任务的相对时间timeout.deadline除以时间轮刻度大小,得到时钟需要摆动多少次才能执行该任务.
  3. 由于在任务添加过程中,始终也会摆动,所以此处减去已经摆动的次数tick; 根据差值计算圈数remainingRounds.
  4. 为了避免任务的延迟时间已过,会将已过时的任务放在当前的刻度上执行.
  5. 计算该任务对应数组的位置.病添加到该刻度对应的双向链表中.

任务执行

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);

在WorkThread线程中,只要当前状态为WORKER_STATE_STARTED,就一直运行该循环. bucket.expireTimeouts(deadline);进行任务执行操作

public void expireTimeouts(long deadline) {
    HashedWheelTimeout timeout = head;

    while (timeout != null) {
        HashedWheelTimeout next = timeout.next;
        if (timeout.remainingRounds <= 0) {
            next = remove(timeout);
            if (timeout.deadline <= deadline) {
                timeout.expire();
            } else {

                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;
    }
}
  1. 从双向链表的head节点开始循环取出任务,如果remainingRounds(圈数小于等于0)则从链表中移除该任务,则执行任务.
  2. 移除取消的任务.timeout.isCancelled()
  3. remainingRounds 递减.

tips

startTime属性是否必须添加volatile.

众所周知volatile是java里面用来保证可见性的关键字.那么对于volatile的具体原理,此处不在详解.对应happens-before相对应的几条规则.

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读
  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  5. start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  6. Join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

由于CoutDownLatch保证了原子性.所以对应规则2,结合规则1.4来看.由于A happens-before B且B happens-before C,所以A happens-before C的.所以workThread对于startTime的修改必定能保证任务提交线程对startTime的读可见.

内存检测检测 ResourceLeakTracker

关于内存泄漏这块逻辑,本人目前暂时不太熟悉.但是能体会到netty作者的设计思想,因为在netty中存在很多的内存池.要想不发生OOM,那么必定存在一套完善的监控.由此可见netty是非常值得深入研究的. 我们看到的可能只是沧海一粟,要学得还很多.

结语:心无惧,方能无敌于天下.

猜你喜欢

转载自juejin.im/post/7126182780421013535