Java排程執行緒池ScheduledThreadPoolExecutor原始碼分析

凌風郎少發表於2019-03-01

最近新接手的專案裡大量使用了ScheduledThreadPoolExecutor類去執行一些定時任務,之前一直沒有機會研究這個類的原始碼,這次趁著機會好好研讀一下。

原文地址:www.jianshu.com/p/18f4c95ac…

該類主要還是基於ThreadPoolExecutor類進行二次開發,所以對Java執行緒池執行過程還不瞭解的同學建議先看看我之前的文章。
當面試官問執行緒池時,你應該知道些什麼?

一、執行流程

  1. 與ThreadPoolExecutor不同,向ScheduledThreadPoolExecutor中提交任務的時候,任務被包裝成ScheduledFutureTask物件加入延遲佇列並啟動一個woker執行緒。

  2. 使用者提交的任務加入延遲佇列時,會按照執行時間進行排列,也就是說佇列頭的任務是需要最早執行的。而woker執行緒會從延遲佇列中獲取任務,如果已經到了任務的執行時間,則開始執行。否則阻塞等待剩餘延遲時間後再嘗試獲取任務。

  3. 任務執行完成以後,如果該任務是一個需要週期性反覆執行的任務,則計算好下次執行的時間後會重新加入到延遲佇列中。

二、原始碼深入分析

首先看下ScheduledThreadPoolExecutor類的幾個建構函式:

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

    public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory);
    }

    public ScheduledThreadPoolExecutor(int corePoolSize,
                                       RejectedExecutionHandler handler) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), handler);
    }

    public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory,
                                       RejectedExecutionHandler handler) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory, handler);
    }複製程式碼

注:這裡建構函式都是使用super,其實就是ThreadPoolExecutor的建構函式
這裡有三點需要注意:

  1. 使用DelayedWorkQueue作為阻塞佇列,並沒有像ThreadPoolExecutor類一樣開放給使用者進行自定義設定。該佇列是ScheduledThreadPoolExecutor類的核心元件,後面詳細介紹。
  2. 這裡沒有向使用者開放maximumPoolSize的設定,原因是DelayedWorkQueue中的元素在大於初始容量16時,會進行擴容,也就是說佇列不會裝滿,maximumPoolSize引數即使設定了也不會生效。
  3. worker執行緒沒有回收時間,原因跟第2點一樣,因為不會觸發回收操作。所以這裡的執行緒存活時間都設定為0。

再次說明:上面三點的理解需要先了解ThreadPoolExecutor的知識點。

當我們建立出一個排程執行緒池以後,就可以開始提交任務了。這裡依次分析一下三個常用API的原始碼:

首先是schedule方法,該方法是指任務在指定延遲時間到達後觸發,只會執行一次。

    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay,
                                       TimeUnit unit) {
        //引數校驗
        if (command == null || unit == null)
            throw new NullPointerException();
        //這裡是一個巢狀結構,首先把使用者提交的任務包裝成ScheduledFutureTask
        //然後在呼叫decorateTask進行包裝,該方法是留給使用者去擴充套件的,預設是個空方法
        RunnableScheduledFuture<?> t = decorateTask(command,
            new ScheduledFutureTask<Void>(command, null,
                                          triggerTime(delay, unit)));
        //包裝好任務以後,就進行提交了
        delayedExecute(t);
        return t;
    }複製程式碼

重點看一下提交任務的原始碼:

    private void delayedExecute(RunnableScheduledFuture<?> task) {
        //如果執行緒池已經關閉,則使用拒絕策略把提交任務拒絕掉
        if (isShutdown())
            reject(task);
        else {
            //與ThreadPoolExecutor不同,這裡直接把任務加入延遲佇列
            super.getQueue().add(task);
            //如果當前狀態無法執行任務,則取消
            if (isShutdown() &&
                !canRunInCurrentRunState(task.isPeriodic()) &&
                remove(task))
                task.cancel(false);
            else
                //這裡是增加一個worker執行緒,避擴音交的任務沒有worker去執行
                //原因就是該類沒有像ThreadPoolExecutor一樣,woker滿了才放入佇列
                ensurePrestart();
        }
    }複製程式碼

這裡的關鍵點其實就是super.getQueue().add(task)行程式碼,ScheduledThreadPoolExecutor類在內部自己實現了一個基於堆資料結構的延遲佇列。add方法最終會落到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();
                //元素數量加1
                size = i + 1;
                //如果當前佇列還沒有元素,則直接加入頭部
                if (i == 0) {
                    queue[0] = e;
                    //記錄索引
                    setIndex(e, 0);
                } else {
                    //把任務加入堆中,並調整堆結構,這裡就會根據任務的觸發時間排列
                    //把需要最早執行的任務放在前面
                    siftUp(i, e);
                }
                //如果新加入的元素就是佇列頭,這裡有兩種情況
                //1.這是使用者提交的第一個任務
                //2.新任務進行堆調整以後,排在佇列頭
                if (queue[0] == e) {
                    //這個變數起優化作用,後面說
                    leader = null;
                    //加入元素以後,喚醒worker執行緒
                    available.signal();
                }
            } finally {
                lock.unlock();
            }
            return true;
        }複製程式碼

通過上面的邏輯,我們把提交的任務成功加入到了延遲佇列中,前面說了加入任務以後會開啟一個woker執行緒,該執行緒的任務就是從延遲佇列中不斷取出任務執行。這些都是跟ThreadPoolExecutor相同的,我們看下從該延遲佇列中獲取元素的原始碼:

        public RunnableScheduledFuture<?> take() throws InterruptedException {
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                for (;;) {
                    //取出佇列中第一個元素,即最早需要執行的任務
                    RunnableScheduledFuture<?> first = queue[0];
                    //如果佇列為空,則阻塞等待加入元素時喚醒
                    if (first == null)
                        available.await();
                    else {
                        //計算任務執行時間,這個delay是當前時間減去任務觸發時間
                        long delay = first.getDelay(NANOSECONDS);
                        //如果到了觸發時間,則執行出隊操作
                        if (delay <= 0)
                            return finishPoll(first);
                        first = null; 
                        //這裡表示該任務已經分配給了其他執行緒,當前執行緒等待喚醒就可以
                        if (leader != null)
                            available.await();
                        else {
                            //否則把給任務分配給當前執行緒
                            Thread thisThread = Thread.currentThread();
                            leader = thisThread;
                            try {
                                //當前執行緒等待任務剩餘延遲時間
                                available.awaitNanos(delay);
                            } finally {
                                //這裡執行緒醒來以後,什麼時候leader會發生變化呢?
                                //就是上面的新增任務的時候
                                if (leader == thisThread)
                                    leader = null;
                            }
                        }
                    }
                }
            } finally {
                //如果佇列不為空,則喚醒其他woker執行緒
                if (leader == null && queue[0] != null)
                    available.signal();
                lock.unlock();
            }
        }複製程式碼

這裡為什麼會加入一個leader變數來分配阻塞佇列中的任務呢?原因是要減少不必要的時間等待。比如說現在佇列中的第一個任務1分鐘後執行,那麼使用者提交新的任務時會不斷的加入woker執行緒,如果新提交的任務都排在佇列後面,也就是說新的woker現在都會取出這第一個任務進行執行延遲時間的等待,當該任務到觸發時間時,會喚醒很多woker執行緒,這顯然是沒有必要的。

當任務被woker執行緒取出以後,會執行run方法,由於此時任務已經被包裝成了ScheduledFutureTask物件,那我們來看下該類的run方法:

        public void run() {
            boolean periodic = isPeriodic();
            //如果當前執行緒池已經不支援執行任務,則取消
            if (!canRunInCurrentRunState(periodic))
                cancel(false);
            else if (!periodic)
                //如果不需要週期性執行,則直接執行run方法然後結束
                ScheduledFutureTask.super.run();
            else if (ScheduledFutureTask.super.runAndReset()) {
                //如果需要週期執行,則在執行完任務以後,設定下一次執行時間
                setNextRunTime();
                //把任務重新加入延遲佇列
                reExecutePeriodic(outerTask);
            }
        }複製程式碼

上面就是schedule方法完整的執行過程。

ScheduledThreadPoolExecutor類中關於週期性執行的任務提供了兩個方法scheduleAtFixedRate跟scheduleWithFixedDelay,一起看下區別。

    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit) {
        //刪除不必要的邏輯,重點看區別
        ScheduledFutureTask<Void> sft =
            new ScheduledFutureTask<Void>(command,
                                          null,
                                          triggerTime(initialDelay, unit),
                                          //二者唯一區別
                                          unit.toNanos(period));
        //...
    }

    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit) {
        //...
        ScheduledFutureTask<Void> sft =
            new ScheduledFutureTask<Void>(command,
                                          null,
                                          triggerTime(initialDelay, unit),
                                          //二者唯一區別
                                          unit.toNanos(-delay));
       //..
    }複製程式碼

前者把週期延遲時間傳入ScheduledFutureTask中,而後者卻設定成負數傳入,區別在哪裡呢?看下當任務執行完成以後的收尾工作中設定任務下次執行時間的方法setNextRunTime原始碼:

        private void setNextRunTime() {
            long p = period;
            //大於0是scheduleAtFixedRate方法,表示執行時間是根據初始化引數計算的
            if (p > 0)
                time += p;
            else
            //小於0是scheduleWithFixedDelay方法,表示執行時間是根據當前時間重新計算的
                time = triggerTime(-p);
        }複製程式碼

也就是說當使用scheduleAtFixedRate方法提交任務時,任務後續執行的延遲時間都已經確定好了,分別是initialDelay,initialDelay + period,initialDelay + 2 * period以此類推。
而呼叫scheduleWithFixedDelay方法提交任務時,第一次執行的延遲時間為initialDelay,後面的每次執行時間都是在前一次任務執行完成以後的時間點上面加上period延遲執行。

三、總結

ScheduledThreadPoolExecutor可以說是在ThreadPoolExecutor上面進行了一些擴充套件操作,它只是重新包裝了任務以及阻塞佇列。該類的阻塞佇列DelayedWorkQueue是基於堆去實現的,本文沒有太詳細介紹堆結構插入跟刪除資料的調整工作,感興趣的同學可以私信或者評論交流。

相關文章