日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > windows >内容正文

windows

Netty源码学习9——从Timer到ScheduledThreadPoolExecutor到HashedWheelTimer

發布時間:2023/12/29 windows 34 coder
生活随笔 收集整理的這篇文章主要介紹了 Netty源码学习9——从Timer到ScheduledThreadPoolExecutor到HashedWheelTimer 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

系列文章目錄和關于我

一丶前言

之前在學習netty源碼的時候,經常看netty hash時間輪(HashedWheelTimer)的出現,時間輪作為一種定時調度機制,在jdk中還存在Timer和ScheduledThreadPoolExecutor。那么為什么netty要重復造*昵,HashedWheelTimer又是如何實現的,解決了什么問題?

這一篇將從Timer-->ScheduledThreadPoolExecutor-->HashedWheelTimer 依次進行講解,學習其中優秀的設計。

二丶Timer

1.基本結構

Timer 始于java 1.3,原理和內部結構也相對簡單,

如上圖所示,Timer內部存在一個線程(TimerThread實例)和一個數組實現的堆

TimerThread運行時會不斷從數組中拿deadline最早的任務,進行執行。為了更快的拿到dealline最早的任務,Timer使用數組構建了一個堆,堆排序的依據便是任務的執行時間。

Timer中只存在一個線程TimerThread來執行定時任務,因此如果一個任務耗時太長會延后其他任務的執行

并且TimerThread不會catch任務執行產生的異常,也就是說如果一個任務執行失敗了,那么TimerThread的執行會終止

2.源碼

2.1 TimerThread 的執行

如下是TimerThread 執行的源碼

  • 基于等待喚醒機制,避免無意義自旋
  • 每次都拿任務隊列中ddl最早的任務
  • 如果周期任務,會計算下一次執行時間,重新塞到任務隊列中
  • 巧妙的使用了 period 等于0,小于0,大于0進行非周期運行任務,fixed delay,fixed rate的區分
private void mainLoop() {
    while (true) {
        try {
            TimerTask task;
            boolean taskFired;
            // 對隊列上鎖,也就是提交任務和拿任務是串行的
            synchronized(queue) {
                // 如果Timer被關閉newTasksMayBeScheduled會為false
                // 這里使用等待喚醒機制來阻塞TimerThread直到存在任務
                while (queue.isEmpty() && newTasksMayBeScheduled)
                    queue.wait();
                // 說明newTasksMayBeScheduled 為false 且沒任務,那么TimerTask的死循環被break,
                if (queue.isEmpty())
                    break; 
                long currentTime, executionTime;
                task = queue.getMin();
                
                // 對任務上鎖,避免并發執行,TimerTask 使用state記錄任務狀態
                synchronized(task.lock) {
                    // 任務取消
                    if (task.state == TimerTask.CANCELLED) {
                        queue.removeMin();
                        continue; 
                    }
                   
                    currentTime = System.currentTimeMillis();
                    executionTime = task.nextExecutionTime;
                    // 需要執行
                    if (taskFired = (executionTime<=currentTime)) {
                        // task.period == 0 說明不是周期執行的任務
                        if (task.period == 0) { 
                            queue.removeMin();
                            task.state = TimerTask.EXECUTED;
                        } else { 
                            // task.period  小于0 那么是fixed-delay ,
                            //  task.period 大于0 那么是fixed-rate
                            // 如果是周期性的,會再次塞到任務隊列中
                            queue.rescheduleMin(
                              task.period<0 ? currentTime   - task.period
                                            : executionTime + task.period);
                        }
                    }
                }
                // 沒到執行的時間,那么等待
                if (!taskFired) 
                    queue.wait(executionTime - currentTime);
            }
            // 到這里會釋放鎖 ,因為任務的執行不需要鎖
            // 任務執行
            if (taskFired)  
                task.run();
        } catch(InterruptedException e) {
        }
    }
}

這段代碼筆者認為有一點可以優化的,那就是在判斷任務是否需要執行,根據period計算執行時間的時候,會在持有任務隊列鎖的情況下,拿任務鎖執行——但是判斷任務是否需要執行,根據period計算執行時間 這段時間其實是可以釋放隊列鎖的!這樣并發的能力可以更強一點,可能Timer的定位也不是應用在高并發任務提交執行的場景,畢竟內部也只有一個線程,所以也無傷大雅。

2.2 任務的提交

任務的提交最終都調用到sched(TimerTask task, long time, long period)方法

這里比較有趣的是,加入到隊列后,會判斷當前任務是不是調度時間最早的任務,如果是那么進行喚醒!這么處理的原因可見下圖解釋:

同樣我不太理解為什么,Timer的作者要拿到隊列鎖,后拿任務鎖,去復制TimerTask的屬性,完全可以將TimerTask的修改放在隊列鎖的外面,如下

2.3 隊列實現的堆

可以看到新增任務需要進行fixUp,調整數組中的元素,實現小根堆,這里時間復雜度是logN

3.Timer的不足

  • 單線程:如果存在多個定時任務,那么后面的定時任務會由于前面任務的執行而delay
  • 錯誤傳播:一個定時任務執行失敗,那么會導致Timer的結束
  • 不友好的API:使用Timer執行延遲任務,需要程序員將任務保證為TimerTask,并且TimerTask無法獲取延遲任務結果

三丶ScheduledThreadPoolExecutor

java 1.5引入的juc工具包,其中ScheduledThreadPoolExecutor就提供了定時調度的能力

  • 其繼承了ThreadPoolExecutor,具備多線程并發執行任務的能力。
  • 更強的錯誤恢復:如果一個任務拋出異常,并不會影響調度器本身和其他任務
  • 更友好的API:支持傳入Runnable,和Callable,調度線程將返回ScheduledFuture,我們可以通過ScheduledFuture來查看任務執行狀態,以及獲取任務結果

由于ScheduledThreadPoolExecutor繼承了ThreadPoolExecutor,其中執行任務的線程運行邏輯同ThreadPoolExecutor(《JUC源碼學習筆記5——1.5w字和你一起刨析線程池ThreadPoolExecutor源碼,全網最細doge》)

1.基本結構

ScheduleThreadPoolExecutor內部結構和ThreadPoolExecutor類似,不同的是內部的阻塞隊列是DelayedWorkQueue——基于數組實現的堆,依據延遲時間進行排序,堆頂,依據Condition等待喚醒機制實現的阻塞隊列;另外堆中的元素是ScheduledFuture

2.源碼

2.1 ScheduledFutureTask的執行

public void run() {
    // 是否周期性,就是判斷period是否為0。
    boolean periodic = isPeriodic();
    // 檢查任務是否可以被執行。
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    // 如果非周期性任務直接調用run運行即可。
    else if (!periodic)
        ScheduledFutureTask.super.run();
    // 如果成功runAndRest,則設置下次運行時間并調用reExecutePeriodic。
        else if (ScheduledFutureTask.super.runAndReset()) {
        setNextRunTime();
        // 需要重新將任務放到工作隊列中
        reExecutePeriodic(outerTask);
    }
}

可以看到任務實現周期執行的關鍵在于任務執行完后會再次被放到延遲阻塞隊列中,ScheduledFutureTask的父類是FutureTask,其內部使用volatile修飾的狀態字段來記錄任務運行狀態,使用cas避免任務重復執行(詳細可看《JUC源碼學習筆記7——FutureTask源碼解析》)

2.2 DelayedWorkQueue

交給ScheduledThreadPoolExecutor執行的任務,都放在DelayedWorkQueue中,下面我們看看DelayedWorkQueue是如何接收任務,以及獲取任務的邏輯

2.2.1 offer接收任務
public boolean offer(Runnable x) {
    if (x == null)
        throw new NullPointerException();
    RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        int i = size;
        if (i >= queue.length)
            // 容量擴增50%。
            grow();
        size = i + 1;
        // 第一個元素
        if (i == 0) {
            queue[0] = e;
            setIndex(e, 0);
        } else {
            // 插入堆尾。
            siftUp(i, e);
        }
        // 如果新加入的元素成為了堆頂,則原先的leader就無效了。
        if (queue[0] == e) {
            leader = null;
            // 那么進行喚醒,因為加入的任務延遲時間是最短的,可能之前隊列存在一個延遲時間更長的任務,導致有線程block了,這時候需要進行喚醒
            available.signal();
        }
    } finally {
        lock.unlock();
    }
    return true;
}

可以看到大致原理和Timer中的阻塞隊列類似,但是其中出現了leader(DelayedWorkQueue中的Thread類型屬性)目前我們還不直到此屬性的作用,需要我們結合take源碼進行分析

2.2.2 take獲取任務
public RunnableScheduledFuture<?> take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    // 上鎖
    lock.lockInterruptibly();
    try {
        for (;;) {
            // 堆頂元素,也就是延遲最小的元素,馬上就要執行的任務
            RunnableScheduledFuture<?> first = queue[0];
            // 如果當前隊列無元素,則在available條件上無限等待直至有任務通過offer入隊并喚醒。
            if (first == null)
                available.await();
            else {
                // 延遲最小任務的延遲
                long delay = first.getDelay(NANOSECONDS);
                // 如果delay小于0說明任務該立刻執行了。
                if (delay <= 0)
                    // 從堆中移除元素并返回結果。
                    return finishPoll(first);

                first = null;
                // 如果目前有leader的話,當前線程作為follower在available條件上無限等待直至喚醒。
                if (leader != null)
                    available.await();
                else {
                    // 如果沒用leader 那么當前線程設置為leader,
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        // 進行超時等待喚醒 ,等待直到可以執行,or存在其他需要更早的任務被add進行隊列
                        available.awaitNanos(delay);
                    } finally {
                        // 如果喚醒之后leader 還是自己那么設置為null
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
       // leader為null ,隊列頭部存在任務,那么喚醒一個線程來獲取任務
        if (leader == null && queue[0] != null)
            available.signal();
        // 如果leader 不為null,或者隊列沒用元素,那么直接釋放鎖
        lock.unlock();
    }
}

整個原理看下來并不復雜,無非是以及Condition提供的等待喚醒機制實現任務的延遲的執行。

但是代碼中存在leader相關的操作,這才是DelayedWorkQueue的精華,下面我們對這個leader機制進行學習

2.2.3 Leader-Follower 模式

DelayedWorkQueue中的leader是一個Thread類型的屬性,它指向了用于在隊列頭等待任務的線程。用于最小化不必要的定時等待

當一個線程成為leader線程時,它只等待下一個延遲過去,而其他線程則無限期地等待。在leader從take或poll返回之前,leader線程必須向其他線程發出信號,除非其他線程在此期間成為引導線程。每當隊列的頭被一個過期時間較早的任務替換時,leader字段就會通過重置為null而無效,并向一些等待線程(但不一定是當前的leader)發出信號。

這么說可能不是很好理解,我們結合代碼進行分析,如下是take中的一段:

  • 如果leader 不為null,讓前來拿任務的線程無限期等待

    • 為什么要這么做——減少無意義的鎖競爭,最早執行的任務已經分配給leader了,

      follower只需要等著即可

    • follower等什么?——等leader拿到任務后進行喚醒,leader拿到任務,那么接下來follower需要執行后續的任務了;或者堆中插入了另外一個延遲時間更小的任務

  • 如果leader為null,那么當前線程成為leader

    • 這意味著堆頂延遲時間最短的任務交由當前線程執行,當前線程只需要等待堆頂任務延遲時間結束即可

    • leader什么時候被喚醒:

      延遲時間到,或者堆中插入了另外一個延遲時間更小的任務

這里就可以看出Leader-Follower是怎么減少無意義的鎖競爭的,leader是身先士卒的將第一個任務攔在身上,讓自己的Follower可以進行永久的睡眠(超時等待),只有leader拿到任務準備執行了,才會喚醒自己的Follower——太溫柔了,我哭死。下面我們看看leader喚醒Follower的代碼

上面展示了leader任務到時間后的代碼邏輯,可以看到leader任務到期后會設置leader為null(這象征了leader的交接,leader去執行任務了,找一個follower做副手),然后如果堆中有任務,那么喚醒一個follower,緊接著前leader就可以執行任務了

其實還存在另外一種case,那就是leader在awaitNanos的中途,存在另外一個更加緊急的任務被塞到堆中

可以看到這里的leader-follower模式,可以有效的減少鎖競爭,因為leader在拿到任務后會喚醒一個線程,從而讓follower可以await,而不是無意義的獲取DelayedWorkQueue的鎖,看有沒有任務需要執行!

  • 優點

    • 減少鎖競爭:通過減少同時嘗試獲取下一個到期任務的線程數量,降低了鎖競爭,從而提高了并發性能。
    • 節省資源:避免多個線程在相同的時間點上喚醒,減少了因競爭而造成的資源浪費。
    • 更好的響應性:由于 leader 線程是唯一等待到期任務的線程,因此它能夠快速響應任務的到期并執行它,而無需從多個等待線程中選擇一個來執行任務。
  • 缺點

    • 潛在的延遲:如果 leader 線程因為其他原因被阻塞或者執行緩慢,它可能會延遲其他任務的執行,因為沒有其他線程在等待那個特定的任務到期(比如leader倒霉的很久沒用獲得cpu時間片)。
    • 復雜性增加:實現 leader-follower 模式需要更多的邏輯來跟蹤和管理 leader 狀態,這增加了代碼的復雜性。(代碼初看,完全看球不同)
    • 故障點:leader 線程可能成為單點故障。如果 leader 線程異常退出或被中斷,必須有機制來確保另一個線程能夠取代它成為新的 leader。(這里使用的finally關鍵字)

最后,在DelayQueue中也使用了leader-follower來進行性能優化

3.ScheduledThreadPoolExecutor優缺點

  • 優點

    • 任務調度: ScheduledThreadPoolExecutor 允許開發者調度一次性或重復執行的任務,這些任務可以基于固定的延遲或固定的頻率來運行。
    • 線程復用: 它維護了一個線程池,這樣線程就可以被復用來執行多個任務,避免了為每個任務創建新線程的開銷。
    • 并發控制: 線程池提供了一個限制并發線程數量的機制,這有助于控制資源使用,提高系統穩定性。
    • 性能優化: 使用內部 DelayedWorkQueue 來管理延遲任務,可以減少不必要的線程喚醒,從而提高性能。
    • 任務同步: ScheduledThreadPoolExecutor 提供了一種機制來獲取任務的結果或取消任務,通過返回的 ScheduledFuture對象可以控制任務的生命周期。
    • 異常處理: 它提供了鉤子方法(如 afterExecute),可以用來處理任務執行過程中未捕獲的異常。
  • 缺點

    • 資源限制: 如果任務執行時間過長或者任務提交速度超過線程池的處理能力,那么線程池可能會飽和,導致性能下降或新任務被拒絕。

      DelayedWorkQueue是*隊列,因此任務都會由核心線程執行,大量提交的時候沒用辦法進行線程的增加

    • 存在大量定時任務提交的時候,性能較低:基于數組實現的堆,調整的時候需要logN的時間復雜度完成

四丶HashedWheelTimer 時間輪

1.引入

筆者學習HashedWheelTimer的時候,問chatgpt netty在哪里使用了時間輪,chatgpt說在IdleStateHandler(當通道有一段時間未執行讀取、寫入時,觸發IdleStateEvent,也就是空閑檢測機制),但是其實在netty的IdleStateHandler并不是使用HashedWheelTimer實現的空閑檢測,依舊是類似ScheduledThreadPoolExecutor的機制(內部使用基于數組實現的堆)

筆者就質問chagpt:"你放屁.jpg"

chatgpt承認了錯誤,然后說它推薦這么做,因為HashedWheelTimer在處理大量延遲任務的時候性能優于基于數組實現的堆。

下面我們就來學習為什么時間輪在處理大量延遲任務的時候性能優于基于數組實現的堆。

2.時間輪算法

時間輪算法(Timewheel Algorithm)是一種用于管理定時任務的高效數據結構,它的核心思想是將時間劃分為一系列的槽(slots),每個槽代表時間輪上的一個基本時間單位。時間輪算法的主要作用是優化計時器任務調度的性能,尤其是在處理大量短時任務時,相比于傳統的數據結構(如最小堆),它能顯著降低任務調度的復雜度。

如下是時間輪的簡單示意,可以看到多個任務使用雙向鏈表進行連接

還存在多層次的時間輪(模擬時針分針秒針)對于周期性很長的定時任務,單層時間輪可能會導致槽的數量過多。為了解決這個問題,可以使用多層時間輪,即每個槽代表的時間跨度越來越大,較低層級代表短時間跨度,較高層級代表長時間跨度

從這里可以看出時間輪為什么在存在大量延遲任務的時候性能比堆更好: 時間輪的插入操作通常是常數時間復雜度(O(1)),因為它通過計算定時任務的執行時間與當前時間的差值,將任務放入相應的槽中,這個操作與定時任務的總數無關。 在堆結構中,插入操作的時間復雜度是O(log N),其中N是堆中元素的數量。這是因為插入新元素后,需要通過上浮(或下沉)操作來維持堆的性質

3.HashedWheelTimer基本結構

  • 時間輪(Wheel):

    時間輪是一個固定大小的數組,數組中的每個元素都是一個槽(bucket)。
    每個槽對應一個時間間隔,這個間隔是時間輪的基本時間單位。
    所有的槽合起來構成了整個時間輪的范圍,例如,如果每個槽代表一個毫秒,那么一個大小為1024的時間輪可以表示1024毫秒的時間范圍。

  • 槽(Bucket):每個槽是一個鏈表,用于存儲所有計劃在該槽時間到期的定時任務。

    任務通過計算它們的延遲時間來確定應該放入哪個槽中。

  • 指針(Cursor or Hand):

    時間輪中有一個指針,代表當前的時間標記。這個指針會周期性地移動到下一個槽,模擬時間的前進。每次指針移動都會檢查相應槽中的任務,執行到期的任務。

  • 任務(TimerTask):

    任務通常是實現了TimerTask接口的對象,其中包含了到期執行的邏輯。
    任務還包含了延遲時間和周期性信息,這些信息使得時間輪可以正確地調度每個任務

  • 工作線程(Worker Thread):

    HashedWheelTimer通常包含一個工作線程,它負責推進時間輪的指針,并處理到期的定時任務。

4.使用demo

public class HashedWheelTimerDemo {

    public static void main(String[] args) {
        // 創建HashedWheelTimer
        HashedWheelTimer timer = new HashedWheelTimer();
        
        // 提交一個延時任務,將在3秒后執行
        TimerTask task1 = new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                System.out.println("Task 1 executed after 3 seconds");
            }
        };
        timer.newTimeout(task1, 3, TimeUnit.SECONDS);
        
        // 提交一個周期性執行的任務,每5秒執行一次
        TimerTask task2 = new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                System.out.println("Task 2 executed periodically every 5 seconds");
                // 重新提交任務,實現周期性執行
                timer.newTimeout(this, 5, TimeUnit.SECONDS);
            }
        };
        timer.newTimeout(task2, 5, TimeUnit.SECONDS);
        
        // 注意:在實際應用中,不要忘記最終停止計時器,釋放資源
        // timer.stop();
    }
}

5.源碼

5.1 創建時間輪

HashedWheelTimer構造方法參數有

  • threadFactory:負責new一個thread,這個thread負責推動時鐘指針旋轉。
  • taskExecutor:Executor負責任務到期后任務的執行
  • tickDuration 和 timeUnit 定義了一格的時間長度,默認的就是 100ms。
  • ticksPerWheel 定義了一圈有多少格,默認的就是 512;
  • leakDetection:用于追蹤內存泄漏。
  • maxPendingTimeouts:最大允許等待的 Timeout 實例數,也就是我們可以設置不允許太多的任務等待。如果未執行任務數達到閾值,那么再次提交任務會拋出RejectedExecutionException 異常。默認不限制。

構造方法主要的工作:

  • 創建HashedWheelBucket數組

    每一個元素都是一個雙向鏈表,鏈表中的元素是HashedWheelTimeout

    默認情況下HashedWheelTimer中有512個這樣的元素

  • 創建workerThread,此Thread負責推動時鐘的旋轉,但是并沒用啟動該線程,當第一個提交任務的時候會進行workerThread線程的啟動

5.2 提交延時任務到HashedWheelTimer

  public Timeout newTimeout(TimerTask task, long delay, TimeUnit 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 + ")");
        }
		
      // 啟動workerThread ,只啟動一次
        start();
		// 計算任務ddl
        long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;
        
        // Guard against overflow.
        if (delay > 0 && deadline < 0) {
            deadline = Long.MAX_VALUE;
        }
      // new一個Timeout 加入到timeouts
      // timeouts 是PlatformDependent.newMpscQueue()————多生產,單消費者的阻塞隊列
        HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
        timeouts.add(timeout);
        return timeout;
    }

其中workerThread的啟動如下

至此我們直到延時任務被加入到timeouts,timeouts是一個mpsc隊列,之所以使用mpsc,是因為可能存在多個生產者提交任務,但是消費任務的只有workerThread,mpsc在這種場景下性能更好。

那么workerThread的工作邏輯是什么昵

5.3 workerThread工作

  • waitForNextTick類似于模擬時鐘上指針的走動,依賴Thread#sleep

  • 當到下一個刻度的時候,會先處理下取消的任務,其實就是對應bucket中刪除(雙向鏈表的刪除)

  • 然后將mpsc隊列中的任務都放到buckets中去

    這里使用了mpsc主要是考慮如果沒加一個任務都直接放到時間輪,那么鎖競爭太激烈了,可能會導致搶鎖阻塞了一段時間導致任務超時。有點消息隊列削峰的意思。

  • 接下來就是找到當tick對應的bucket的,然后執行這個bucket中所有需要執行的任務

    可以看到其實就是遍歷雙向鏈表,找到需要執行任務,任務的執行調用expire方法,邏輯如下:

    直接交給線程池執行,之前之前還會嘗試修改狀態,這里其實和用戶取消任務由競爭關系,也就是說如果任務提交到線程池,那么取消也無濟于事了。

6.品一品優秀的設計

筆者認為這里優秀的設計主要是在于MPSC的應用

  • 線程安全: HashedWheelTimer通常由一個工作線程來管理時間輪的推進和執行任務。如果允許多個線程直接在時間輪的桶(bucket)中添加任務,就必須處理并發修改的問題,這將大大增加復雜性和性能開銷。MPSC隊列允許多個生產者線程安全地添加任務,而消費者線程(也就是HashedWheelTimer的工作線程)則負責將這些任務從隊列中取出并放入正確的時間槽中。
  • 性能優化: 使用MPSC隊列可以減少鎖的競爭,從而提高性能。由于任務首先被放入隊列中,工作線程可以在合適的時間批量處理這些任務,這減少了對時間輪數據結構的頻繁鎖定和同步操作。

7.時間輪的優點和缺點

7.1優點

  • 高效的插入和過期檢查: 添加新任務到時間輪的操作是常數時間復雜度(O(1)),而檢查過期任務也是常數時間復雜度,因為只需要檢查當前槽位的任務列表。
  • 可配置的時間粒度: 時間輪的槽數量(時間粒度)是可配置的,可以根據應用程序的需要調整定時器的精度和資源消耗。
  • 處理大量定時任務: HashedWheelTimer尤其適合于需要處理大量定時任務的場景,例如網絡應用中的超時監測。

7.2缺點

  • 有限的時間精度: 由于時間輪是以固定的時間間隔來劃分的,所以它的時間精度受到槽數量和槽間隔的限制,不能提供非常高精度的定時(如毫秒級以下)。這是小根堆優于時間輪的地方
  • 槽位溢出: 單個槽位可能會有多個任務同時過期,如果過期任務的數量非常大,可能會導致任務處理的延遲。這里netty使用線程去執行任務,但是線程池可能存在沒用可用線程帶來的延遲
  • 系統負載敏感: 當系統負載較高時,定時器的準確性可能會降低,因為HashedWheelTimer的工作線程可能無法準確地按照預定的時間間隔推進時間輪。
  • 任務延遲執行: 如果任務在其預定的執行時間點添加到時間輪,可能會出現任務執行時間稍微延后的情況,因為會先塞到MPSC然后等下一個tick才被放到bucket然后才能被執行。

在選擇使用HashedWheelTimer時,需要根據應用場景的具體需求權衡這些優缺點。對于需要處理大量網絡超時檢測的場景,HashedWheelTimer常常是一個合適的選擇。然而,如果應用程序需要高度精確的定時器,或者對任務執行的實時性有嚴格的要求,可能需要考慮ScheduledThreadPoolExecutor(Timer就是個垃圾doge)。

五丶思考

ScheduledThreadPoolExecutor和HashedWheelTimer 各有優劣,需要根據使用場景進行權衡

  • 關注任務調度的及時性:選擇ScheduledThreadPoolExecutor
  • 存在大量調度任務:選擇HashedWheelTimer

二者的特性又是由其底層數據結構決定

  • 為了維持小根堆的特性,每次向ScheduledThreadPoolExecutor中新增任務都需要進行調整,在存在大量任務的時候,這個調整的開銷maybe很大(都是內存操作,感覺應該還好)
  • 為了讓任務的新增時間復雜度是o(1),HashedWheelTimer 利用hash和數組o(1)的尋址能力,但是也是因為數組的設計,導致任務的執行需要依賴workerThread每隔一個tick進行調度,喪失了一點任務執行的及時性

這一篇最大的收獲還是ScheduleThreadPoolExecutor中使用的leader-follower模式,以及HashedWheelTimer中mpsc 運用,二者都是在減少無意義的鎖競爭!

總結

以上是生活随笔為你收集整理的Netty源码学习9——从Timer到ScheduledThreadPoolExecutor到HashedWheelTimer的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。