Netty中的时间轮(v3.2.5)

时间轮是一种可以执行定时任务的数据结构和算法.这篇文章,讲解一下它在Netty 3.x系列中如何实现的,它在4.x系列将在后面的文章中讲解.

此次讲解的版本如下​

<dependency>
    <groupId>org.jboss.netty</groupId>
    <artifactId>netty</artifactId>
    <version>3.2.5.Final</version>
</dependency>

它的实现上是一个一维数组,但把它当成一个环形数组使用.如同时间的车轮一样.

图片.png

首先说明一点的是,时间轮是按照顺时针的方向'转动',也就是按照顺时针的方向执行时间轮上的任务.

每一个格子的时间跨度都是tickDuration,就是说图中每个小格子的时间长度都是tickDuration

时间轮在开始'转动'的时候会记录下开始时间startTime.每个格子表示一个tick值,第一个格子的tick值等于1,第二个格子的tick值等于2,以此类推.

如图所示,第9个格子与第1个格子是同一个,第10个格子与第2个是同一个,以此类推.

图片.png

根据上面的几点说明,计算图中K点的时刻
如果tick=1,那么结果等于startTime+tickDuration * 1
如果tick=9,那么结果等于startTime+tickDuration * 9
想想是不是,因为每个格子的时间跨度都是tickDuration,所以经过n个格子,就是tickDuration * n这么长时间,又由于起始时间是startTime,所以每个格子的结束时刻(例如图中K点,Q点)就等于startTime+tickDuration * n

时间轮初始化之后,它的结构如下图

图片.png

假如此时时间轮正在执行下图中S格子中的任务,这时向时间轮中添加一个延时delay的任务,时间轮会根据当前所处的位置和时刻,计算新添加的任务应该放在哪个格子位置.而且新添加的任务的deadline=currentTime + delay.也就是说,当前时间加上dalay延时时间就是这个任务要在那个时刻被执行.

图片.png

时间轮会绑定一个唯一的线程,执行时间轮上的任务.

// 构造器
public HashedWheelTimer(
            ThreadFactory threadFactory,
            long tickDuration, TimeUnit unit, int ticksPerWheel) {

    wheel = createWheel(ticksPerWheel);// 创建时间轮,实际就是一个Set数组
    iterators = createIterators(wheel);// 创建每个Set元素的迭代器
    mask = wheel.length - 1;

    this.tickDuration = tickDuration = unit.toMillis(tickDuration);// 每个格子的时间跨度

    roundDuration = tickDuration * wheel.length; // 转一圈的时间

        // 执行时间轮里面任务的线程
    workerThread = threadFactory.newThread(new ThreadRenamingRunnable(worker, "Hashed wheel timer #" + id.incrementAndGet()));

}

// 源码位置: org.jboss.netty.util.HashedWheelTimer.Worker#run
public void run() {
      List<HashedWheelTimeout> expiredTimeouts = new ArrayList<HashedWheelTimeout>();
      // 记录时间轮启动的开始时间
      startTime = System.currentTimeMillis();
      // 从tick=1的位置开始执行
      tick = 1;
      
      while (!shutdown.get()) {
            // 等到下一个tick的时间到达
            final long deadline = waitForNextTick();
            if (deadline > 0) {
              // 获取到期的任务
              fetchExpiredTimeouts(expiredTimeouts, deadline);
              // 执行到期的任务
              notifyExpiredTimeouts(expiredTimeouts);
            }
      }
}

执行线程会一直等到时间到达当前tick的结束时间.例如之前上面说的K点或Q点位置.​

private long waitForNextTick() {
    // 计算第tick格子的结束时刻
    long deadline = startTime + tickDuration * tick;

    for (;;) {
        final long currentTime = System.currentTimeMillis();
        // 可以等价于startTime + tickDuration * tick - currentTime
        final long sleepTime = tickDuration * tick - (currentTime - startTime);
        // 根据上面的等式,它的意思就是说必须等到时间走到了当前格子的结束时刻才终止.
        if (sleepTime <= 0) {
            break;
        }

        try {
            // 如果时间还没有走到tick格子的结束时刻则sleep
            Thread.sleep(sleepTime);
        } catch (InterruptedException e) {
            if (shutdown.get()) {
                return -1;
            }
        }
    }
    // tick加1
    tick ++;
    return deadline;
}

通过图形的方式解释下上面的方法

图片.png

首先会计算出当前tick的deadline的值,就是图中标注的.如果当前时间(currentTime)还没有走到deadline,那么线程睡眠(deadline-currentTime)这么久,'醒来'之后继续检查.只有当前时间>=deadline的时候,才会跳出循环,开始执行任务.
接下来将当前格子中的所有任务遍历一遍,找出任务的deadline(每个任务在放入时间轮的时候,都会有一个deadline值)比图中deadline小的任务,把它们放入一个集合,然后执行它们.

// 这个方法都在处理同一个格子里面的任务
private void fetchExpiredTimeouts(
                List<HashedWheelTimeout> expiredTimeouts,
                ReusableIterator<HashedWheelTimeout> i, long deadline) {

    List<HashedWheelTimeout> slipped = null;
    i.rewind();
    // 每个格子的底层都是通过HashMap存储任务的,而且每个格子都有一个迭代器,用于迭代HashMap中的任务.
    while (i.hasNext()) {
        HashedWheelTimeout timeout = i.next();
                // 有些任务是在这一圈就要执行,有些任务是要等到第2圈,第3圈...才会执行的.这里只会找出这一圈需要执行的任务
        if (timeout.remainingRounds <= 0) {
            i.remove();
            // 如果任务已经过期,则加入集合中,接下来就会执行这个任务
            if (timeout.deadline <= deadline) {
                expiredTimeouts.add(timeout);
            } else {
                if (slipped == null) {
                    slipped = new ArrayList<HashedWheelTimer.HashedWheelTimeout>();
                }
                // 有些任务可能放错了格子,需要重新放入格子
                slipped.add(timeout);
            }
        } else {
            timeout.remainingRounds --;
        }
    }

    if (slipped != null) {
        for (HashedWheelTimeout timeout: slipped) {
            scheduleTimeout(timeout, timeout.deadline - deadline);
        }
    }
}

执行任务

private void notifyExpiredTimeouts(List<HashedWheelTimeout> expiredTimeouts) {
    // 遍历列表,执行任务
    for (int i = expiredTimeouts.size() - 1; i >= 0; i --) {
        expiredTimeouts.get(i).expire();
    }

    expiredTimeouts.clear();
}

简单做个总结,如下图,时间轮在执行的过程,假如当前正在准备执行tick=2的格子中的任务,如果当前时间没有走到deadline时刻,那么线程睡眠,直到时间到达deadline时刻,那么就开始执行格子中的任务(每个格子中的任务都是外部线程提交到时间轮里的).
首先需要筛选出格子中任务的deadline<=图中的deadline.只有任务的deadline<=图中标记的deadline,才会执行这些满足条件的任务.未满足条件的任务只能等到下一圈,或者下下圈才能被执行,这是根据任务的延时时间决定的.

图片.png

任务在提交到时间轮的时候,它何时被执行,已经被确定了.
假如正在执行S格子中的任务,这个时候添加了一个任务task,它要在D这个时刻被执行,那么当时间走到N这个deadline点的时候,任务task就一定要被执行.

图片.png

一个任务要在延时300ms的时候被执行,但是未必一定是在延时300ms后被执行.只有时间轮的时刻走到了类似图中N点这样的位置的时候,才会执行当前格子(也就是当前tick)中比N点时刻小的任务,就是任务的deadline的时刻比N点的时刻小,才会执行这样的任务.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容