知識圖譜整理之Java基礎ScheduledThreadPoolExecutor

前言

之前主要是想了解Spring下的定時任務機制,但是在看了相關源碼後,發現必須先要了解ScheduledThreadPoolExecutor,之後在閱讀會更加簡單,顧先去看了下這塊的源碼,發現其還是很有意思和學習的地方的。

ScheduledThreadPoolExecutor介紹

我們首先要知道ScheduledThreadPoolExecutor是JUC包下關於定時任務這塊的,不知道大家跟我之前有沒有一樣的疑問,定時任務到底是如何執行的,在ScheduledThreadPoolExecutor就能滿足你這個願望,瞭解其運作原理。

源碼解析

構造方法

這裏的構造方法有4種,我拿一個最全的來距離:

public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory,
                                       RejectedExecutionHandler handler) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory, handler);
    }
  • 可以看到構造函數的入參是隻能傳入核心線程數,線程工廠類和拒絕策略。在之前的文章中我們已經分析了ThreadPoolExecutor,而ScheduledThreadPoolExecutor是繼承了這個類的,所以不去過多解釋參數作用。
  • 我們發現最大線程數設置的是最大的,其實這裏這個參數是無用的,因爲我們知道如果阻塞隊列是無界的話,那麼這個最大線程數參數就是無效的,而這DelayedWorkQueue隊列就是這麼一個無界隊列。

DelayedWorkQueue類源碼

首先這個類是是ScheduledThreadPoolExecutor的內部類,它的數據結構是小頂堆,小頂堆是什麼?

最小堆,是一種經過排序的完全二叉樹,其中任一非終端節點的數據值均不大於其左子節點和右子節點的值。

怎麼理解呢?說直白點就是一顆滿二叉樹,然後高度越高,值越小。
然後是爲什麼要這麼設計呢?因爲是定時任務呀,肯定是現在最短時間執行的排在越上面來進行讀取。這邊就大致講一下,如果有問題的,歡迎交流。

下面直接來看源碼,我們也知道阻塞隊列在ThreadPoolExecutor的運作過程中會涉及到的是take和epoll方法,還有一個就是offer方法,我們就從這些方法入手。

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)
                    grow();
                size = i + 1;
                if (i == 0) {
                    queue[0] = e;
                    setIndex(e, 0);
                } else {
                    siftUp(i, e);
                }
                if (queue[0] == e) {
                    leader = null;
                    available.signal();
                }
            } finally {
                lock.unlock();
            }
            return true;
        }
  • grow是擴容方法,比較的簡單,這裏不過多介紹,有興趣一看就會
  • setIndex方法設計到ScheduledFutureTask,這個後面會講,就是把隊列的索引序號冗餘過去,方便刪除的時候能不需要定位查詢再刪除
  • siftUp就是插入值了,我們之後來看下源碼
siftUp方法源碼
        private void siftUp(int k, RunnableScheduledFuture<?> key) {
            while (k > 0) {
                int parent = (k - 1) >>> 1;
                RunnableScheduledFuture<?> e = queue[parent];
                if (key.compareTo(e) >= 0)
                    break;
                queue[k] = e;
                setIndex(e, k);
                k = parent;
            }
            queue[k] = key;
            setIndex(key, k);
        }
  • 你品一下,再細品一下,其中(k - 1) >>> 1等於(k-1)/2,因爲是小頂堆的數據結構,所以這個值等於k的父節點。
  • 不知道你品出來沒有,我這簡單說下,就是如果是比父節點大的,就直接在指定位置插入,如果不是,就無限循環,把父節點的位置賦值到原先位置,然後向上查找。
take方法源碼
public RunnableScheduledFuture<?> take() throws InterruptedException {
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                for (;;) {
                    RunnableScheduledFuture<?> first = queue[0];
                    if (first == null)
                        available.await();
                    else {
                        long delay = first.getDelay(NANOSECONDS);
                        if (delay <= 0)
                            return finishPoll(first);
                        first = null; // don't retain ref while waiting
                        if (leader != null)
                            available.await();
                        else {
                            Thread thisThread = Thread.currentThread();
                            leader = thisThread;
                            try {
                                available.awaitNanos(delay);
                            } finally {
                                if (leader == thisThread)
                                    leader = null;
                            }
                        }
                    }
                }
            } finally {
                if (leader == null && queue[0] != null)
                    available.signal();
                lock.unlock();
            }
        }
  • 之前也沒有接觸過阻塞隊列是如何實現的,看完這段代碼之後有了更深的瞭解
  • 首先會獲取隊列中的第一個元素,如果是0的話,就會執行available.await()進行阻塞,等待調用 available.signal()方法
  • OK,下面就是精髓了,也是我悟了一會纔出來的精髓,這裏和之前的阻塞隊列不同,這裏如果有值得話,會執行first.getDelay方法,這個作用是獲取這個任務還需要多少時間延遲之後才能執行,如果<=0,則說明可以立即執行了,那麼會調用finishPoll進行一些操作後,把隊頭元素返回。
  • 如果還在延遲的話,注意,後面是精髓,這時leader是爲null的,這個leader是幹嘛的?我們之後就是來講這個leader參數的作用。我們可以看到先是available.awaitNanos(delay)來等待對應的延遲時間,所以正常情況在等待了delay醒來之後,會把leader再置爲null,再循環執行的時候就可以直接取出返回了。
  • 這裏我的說明不知道能不能理解,不理解的話歡迎交流下哦。
  • epoll方法其實和take差不多,這裏不做過多介紹,大家理解下take的情況下epoll也很好理解了。

ScheduledThreadPoolExecutor源碼解析

我們知道這是繼承ThreadPoolExecutor的,顧其使用方式也類似,我們來看下它的execute方法源碼

execute方法源碼
    public void execute(Runnable command) {
        schedule(command, 0, NANOSECONDS);
    }
  • 實際調用的是schedule源碼
schedule方法源碼
    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay,
                                       TimeUnit unit) {
        if (command == null || unit == null)
            throw new NullPointerException();
        RunnableScheduledFuture<?> t = decorateTask(command,
            new ScheduledFutureTask<Void>(command, null,
                                          triggerTime(delay, unit)));
        delayedExecute(t);
        return t;
    }
  • decorateTask方法就是第二個傳入參數,其中ScheduledFutureTask我們之後分析
  • triggerTime就是獲取下次執行的時間,return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
  • delayedExecute則是實際添加運行
delayedExecute方法源碼
    private void delayedExecute(RunnableScheduledFuture<?> task) {
        if (isShutdown())
            reject(task);
        else {
            super.getQueue().add(task);
            if (isShutdown() &&
                !canRunInCurrentRunState(task.isPeriodic()) &&
                remove(task))
                task.cancel(false);
            else
                ensurePrestart();
        }
    }
  • 檢查線程池狀態,如果是shutdown就直接拒絕
  • 隊列添加元素,然後會再確認一遍線程池狀態再調用ensurePrestart方法
ensurePrestart方法源碼
    void ensurePrestart() {
        int wc = workerCountOf(ctl.get());
        if (wc < corePoolSize)
            addWorker(null, true);
        else if (wc == 0)
            addWorker(null, false);
    }
  • 這裏的意思是如果小於核心線程池時或者線程池設置爲0,但是還是最少會有一條線程去執行邏輯

ScheduledFutureTask源碼解析

這個類呢主要是用來實現延遲任務的,來看下繼承圖:
繼承圖

這裏可以看到實現的三個方向是Runable、Future、Delayed,就能大概理解了他的功能。我們主要來看下關於Runable的實現,其他兩個功能的實現也不難,有興趣可以自己看下,有問題的話歡迎交流。

run方法源碼
public void run() {
            boolean periodic = isPeriodic();
            if (!canRunInCurrentRunState(periodic))
                cancel(false);
            else if (!periodic)
                ScheduledFutureTask.super.run();
            else if (ScheduledFutureTask.super.runAndReset()) {
                setNextRunTime();
                reExecutePeriodic(outerTask);
            }
        }
  • 這裏我們可能會對ScheduledFutureTask的父類的run和runAndReset方法有點好奇,主要區別是是否吧result賦值進去
  • setNextRunTime會根據time來計算下一次延遲執行的時間
  • 這裏簡單總結下就是如果是不是週期性運行的,直接調用父類的run方法,然後可以通過get方法獲取result值,如果是週期性的,會先計算下次執行時間, 然後再次把任務放入隊列中。

個人總結

這篇定時任務總結的話,主要還是想要理清定時延遲任務時如何實現的,我們從頭來開始理一下,首先是通過定義一個正常的線程池,這裏的重點區別是放入的阻塞隊列是內部類DelayedWorkQueue,然後回憶下這個類用到的offer、take、poll方法內容。OK,在使用中我們調用execute方法,實際上是封裝成了一個內部的ScheduledFutureTask方法,然後通過其實現週期性運行。延遲的話也是通過ScheduledFutureTask實現了Delayed可獲取到還需延遲的時間。

今日的知識圖譜:
知識圖譜

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章