Netty源碼解析 -- FastThreadLocal與HashedWheelTimer

Netty源碼分析系列文章已接近尾聲,本文再來分析Netty中兩個(gè)常見組件:FastThreadLoca與HashedWheelTimer缅茉。
源碼分析基于Netty 4.1.52

FastThreadLocal

FastThreadLocal比較簡(jiǎn)單。
FastThreadLocal和FastThreadLocalThread是配套使用的埃叭。
FastThreadLocalThread繼承了Thread暂氯,F(xiàn)astThreadLocalThread#threadLocalMap 是一個(gè)InternalThreadLocalMap秸侣,該InternalThreadLocalMap對(duì)象只能用于當(dāng)前線程。
InternalThreadLocalMap#indexedVariables是一個(gè)數(shù)組罢绽,存放了當(dāng)前線程所有FastThreadLocal對(duì)應(yīng)的值畏线。
而每個(gè)FastThreadLocal都有一個(gè)index,用于定位InternalThreadLocalMap#indexedVariables良价。


FastThreadLocal#get

public final V get() {
    // #1
    InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
    // #2
    Object v = threadLocalMap.indexedVariable(index);
    if (v != InternalThreadLocalMap.UNSET) {
        return (V) v;
    }
    // #3
    return initialize(threadLocalMap);
}

#1 獲取該線程的InternalThreadLocalMap
如果是FastThreadLocalThread寝殴,直接獲取FastThreadLocalThread#threadLocalMap。
否則明垢,從UnpaddedInternalThreadLocalMap.slowThreadLocalMap獲取該線程InternalThreadLocalMap杯矩。
注意,UnpaddedInternalThreadLocalMap.slowThreadLocalMap是一個(gè)ThreadLocal袖外,這里實(shí)際回退到使用ThreadLocal了史隆。
#2 每個(gè)FastThreadLocal都有一個(gè)index。
通過該index曼验,獲取InternalThreadLocalMap#indexedVariables中存放的值
#3 找不到值泌射,通過initialize方法構(gòu)建新對(duì)象。

可以看到鬓照,F(xiàn)astThreadLocal中連hash算法都不用熔酷,通過下標(biāo)獲取對(duì)應(yīng)的值,復(fù)雜度為log(1)豺裆,自然很快啦拒秘。

HashedWheelTimer

HashedWheelTimer是Netty提供的時(shí)間輪調(diào)度器。
時(shí)間輪是一種充分利用線程資源進(jìn)行批量化任務(wù)調(diào)度的調(diào)度模型臭猜,能夠高效的管理各種延時(shí)任務(wù)躺酒。
簡(jiǎn)單說,就是將延時(shí)任務(wù)存放到一個(gè)環(huán)形隊(duì)列中蔑歌,并通過執(zhí)行線程定時(shí)執(zhí)行該隊(duì)列的任務(wù)羹应。

例如,
環(huán)形隊(duì)列上有60個(gè)格子次屠,
執(zhí)行線程每秒移動(dòng)一個(gè)格子园匹,則環(huán)形隊(duì)列每輪可存放1分鐘內(nèi)的任務(wù)雳刺。
現(xiàn)在有兩個(gè)定時(shí)任務(wù)
task1,32秒后執(zhí)行
task2裸违,2分25秒后執(zhí)行
而執(zhí)行線程當(dāng)前位于第6格子
則task1放到32+6=38格掖桦,輪數(shù)為0
task2放到25+6=31個(gè),輪數(shù)為2
執(zhí)行線程將執(zhí)行當(dāng)前格子輪數(shù)為0的任務(wù)供汛,并將其他任務(wù)輪數(shù)減1枪汪。


缺點(diǎn),時(shí)間輪調(diào)度器的時(shí)間精度不高紊馏。
因?yàn)闀r(shí)間輪算法的精度取決于執(zhí)行線程移動(dòng)速度料饥。
例如上面例子中執(zhí)行線程每秒移動(dòng)一個(gè)格子蒲犬,則調(diào)度精度小于一秒的任務(wù)就無(wú)法準(zhǔn)時(shí)調(diào)用朱监。

HashedWheelTimer關(guān)鍵字段

// 任務(wù)執(zhí)行器,負(fù)責(zé)執(zhí)行任務(wù)
Worker worker = new Worker();
// 任務(wù)執(zhí)行線程
Thread workerThread;
//  HashedWheelTimer狀態(tài)原叮, 0 - init, 1 - started, 2 - shut down
int workerState;
// 時(shí)間輪隊(duì)列赫编,使用數(shù)組實(shí)現(xiàn)
HashedWheelBucket[] wheel;
// 暫存新增的任務(wù)
Queue<HashedWheelTimeout> timeouts = PlatformDependent.newMpscQueue();
// 已取消任務(wù)
Queue<HashedWheelTimeout> cancelledTimeouts = PlatformDependent.newMpscQueue();

添加延遲任務(wù) HashedWheelTimer#newTimeout

public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
    ...

    // #1
    start();

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

    ...
    HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
    timeouts.add(timeout);
    return timeout;
}

#1 如果HashedWheelTimer未啟動(dòng),則啟動(dòng)該HashedWheelTimer
HashedWheelTimer#start方法負(fù)責(zé)是啟動(dòng)workerThread線程
#2 startTime是HashedWheelTimer啟動(dòng)時(shí)間
deadline是相對(duì)HashedWheelTimer啟動(dòng)的延遲時(shí)間
構(gòu)建HashedWheelTimeout奋隶,添加到HashedWheelTimer#timeouts

時(shí)間輪運(yùn)行 Worker#run

public void run() {
    ...

    // #1
    startTimeInitialized.countDown();

    do {
        // #2
        final long deadline = waitForNextTick();
        if (deadline > 0) {
            // #3
            int idx = (int) (tick & mask);
            processCancelledTasks();
            HashedWheelBucket bucket = wheel[idx];
            // #4
            transferTimeoutsToBuckets();
            // #5
            bucket.expireTimeouts(deadline);
            // #6
            tick++;
        }
    } while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);

    // #7
    ...
}

#1 HashedWheelTimer#start方法阻塞HashedWheelTimer線程直到Worker啟動(dòng)完成擂送,這里解除HashedWheelTimer線程阻塞。
#2 計(jì)算下一格子開始執(zhí)行的時(shí)間唯欣,然后sleep到下次格子開始執(zhí)行時(shí)間
#2 tick是從HashedWheelTimer啟動(dòng)后移動(dòng)的總格子數(shù)嘹吨,這里獲取tick對(duì)應(yīng)的格子索引。
由于Long類型足夠大境氢,這里并不考慮溢出問題蟀拷。
#4 將HashedWheelTimer#timeouts的任務(wù)遷移到對(duì)應(yīng)的格子中
#5 處理已到期任務(wù)
#6 移動(dòng)到下一個(gè)格子
#7 這里是HashedWheelTimer#stop后的邏輯處理,取消任務(wù)萍聊,停止時(shí)間輪

遷移任務(wù) Worker#transferTimeoutsToBuckets

private void transferTimeoutsToBuckets() {
    // #1
    for (int i = 0; i < 100000; i++) {
        HashedWheelTimeout timeout = timeouts.poll();
        if (timeout == null) {
            // all processed
            break;
        }
        if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) {
            continue;
        }
        // #2
        long calculated = timeout.deadline / tickDuration;
        // #3
        timeout.remainingRounds = (calculated - tick) / wheel.length;
        // #4
        final long ticks = Math.max(calculated, tick); // Ensure we don't schedule for past.
        // #5
        int stopIndex = (int) (ticks & mask);

        HashedWheelBucket bucket = wheel[stopIndex];
        bucket.addTimeout(timeout);
    }
}

#1 注意问芬,每次只遷移100000個(gè)任務(wù),以免阻塞線程
#2 任務(wù)延遲時(shí)間/每格時(shí)間數(shù)寿桨, 得到該任務(wù)需延遲的總格子移動(dòng)數(shù)
#3 (總格子移動(dòng)數(shù) - 已移動(dòng)格子數(shù))/每輪格子數(shù)此衅,得到輪數(shù)
#4 如果任務(wù)在timeouts隊(duì)列放得太久導(dǎo)致已經(jīng)過了執(zhí)行時(shí)間,則使用當(dāng)前tick亭螟, 也就是放到當(dāng)前bucket挡鞍,以便盡快執(zhí)行該任務(wù)
#5 計(jì)算tick對(duì)應(yīng)格子索引,放到對(duì)應(yīng)的格子位置

執(zhí)行到期任務(wù) HashedWheelBucket#expireTimeouts

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

    while (timeout != null) {
        HashedWheelTimeout next = timeout.next;
        // #1
        if (timeout.remainingRounds <= 0) {
            // #2
            next = remove(timeout);
            if (timeout.deadline <= deadline) {
                // #3
                timeout.expire();
            } else {
                throw new IllegalStateException(String.format(
                        "timeout.deadline (%d) > deadline (%d)", timeout.deadline, deadline));
            }
        } else if (timeout.isCancelled()) {
            next = remove(timeout);
        } else {
            // #4
            timeout.remainingRounds --;
        }
        timeout = next;
    }
}

#1 選擇輪數(shù)小于等于0的任務(wù)
#2 移除任務(wù)
#3 修改狀態(tài)為過期预烙,并執(zhí)行任務(wù)
#4 其他任務(wù)輪數(shù)減1

ScheduledExecutorService使用堆(DelayedWorkQueue)維護(hù)任務(wù)匕累,新增任務(wù)復(fù)雜度為O(logN)。
而 HashedWheelTimer 新增任務(wù)復(fù)雜度為O(1)默伍,所以在任務(wù)非常多時(shí)欢嘿, HashedWheelTimer 可以表現(xiàn)出它的優(yōu)勢(shì)衰琐。
但是任務(wù)較少甚至沒有任務(wù)時(shí),HashedWheelTimer的執(zhí)行線程都需要不斷移動(dòng)炼蹦,也會(huì)造成性能消耗羡宙。
注意,HashedWheelTimer使用同一個(gè)線程調(diào)用和執(zhí)行任務(wù)掐隐,如果某些任務(wù)執(zhí)行時(shí)間過久狗热,則影響后續(xù)定時(shí)任務(wù)執(zhí)行。當(dāng)然虑省,我們也可以考慮在任務(wù)中另起線程執(zhí)行邏輯匿刮。
另外,如果任務(wù)過多探颈,也會(huì)導(dǎo)致任務(wù)長(zhǎng)期滯留在HashedWheelTimer#timeouts中而不能及時(shí)執(zhí)行熟丸。

如果您覺得本文不錯(cuò),歡迎關(guān)注我的微信公眾號(hào)伪节,系列文章持續(xù)更新中光羞。您的關(guān)注是我堅(jiān)持的動(dòng)力!


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末怀大,一起剝皮案震驚了整個(gè)濱河市纱兑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌化借,老刑警劉巖潜慎,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蓖康,居然都是意外死亡铐炫,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門钓瞭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來驳遵,“玉大人,你說我怎么就攤上這事山涡〉探幔” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵鸭丛,是天一觀的道長(zhǎng)竞穷。 經(jīng)常有香客問我,道長(zhǎng)鳞溉,這世上最難降的妖魔是什么瘾带? 我笑而不...
    開封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮熟菲,結(jié)果婚禮上看政,老公的妹妹穿的比我還像新娘朴恳。我一直安慰自己,他們只是感情好允蚣,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開白布于颖。 她就那樣靜靜地躺著,像睡著了一般嚷兔。 火紅的嫁衣襯著肌膚如雪森渐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天冒晰,我揣著相機(jī)與錄音同衣,去河邊找鬼。 笑死壶运,一個(gè)胖子當(dāng)著我的面吹牛耐齐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播前弯,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蚪缀,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼秫逝!你這毒婦竟也來了恕出?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤违帆,失蹤者是張志新(化名)和其女友劉穎浙巫,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體刷后,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡的畴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了尝胆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丧裁。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖含衔,靈堂內(nèi)的尸體忽然破棺而出煎娇,到底是詐尸還是另有隱情,我是刑警寧澤贪染,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布缓呛,位于F島的核電站,受9級(jí)特大地震影響杭隙,放射性物質(zhì)發(fā)生泄漏哟绊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一痰憎、第九天 我趴在偏房一處隱蔽的房頂上張望票髓。 院中可真熱鬧攀涵,春花似錦、人聲如沸洽沟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)玲躯。三九已至据德,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間跷车,已是汗流浹背棘利。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留朽缴,地道東北人善玫。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像密强,于是被迫代替她去往敵國(guó)和親茅郎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容