線程池之ScheduledThreadPoolExecutor調度原理

ScheduledThreadPoolExecutor 的調度原理主要基于兩個內部類,ScheduledFutureTask 和 DelayedWorkQueue:

  1. ScheduledFutureTask 是對任務的一層封裝,將我們提交的 Runnable 或 Callable 封裝成具有時間周期的任務;
  2. DelayedWorkQueue 實現了對 ScheduledFutureTask 的延遲出隊管理;

ScheduledFutureTask

ScheduledFutureTask類圖

ScheduledFutureTask有以下幾種構造方法:

ScheduledFutureTask(Runnable r, V result, long ns) {
    super(r, result);
    this.time = ns;
    this.period = 0;
    this.sequenceNumber = sequencer.getAndIncrement();
}

ScheduledFutureTask(Runnable r, V result, long ns, long period) {
    super(r, result);
    this.time = ns;
    this.period = period;
    this.sequenceNumber = sequencer.getAndIncrement();
}

ScheduledFutureTask(Callable<V> callable, long ns) {
    super(callable);
    this.time = ns;
    this.period = 0;
    this.sequenceNumber = sequencer.getAndIncrement();
}

super 中調用 FutureTask 的構造方法,可以參考 FutureTask實現原理。ScheduledFutureTask 主要配置參數如下:

名稱 含義
time 任務能夠執行的時間點(單位:nanoTime )
period 正值表示固定時間周期執行。
負值表示固定延遲周期執行。
0表示非重復任務。
sequenceNumber FIFO調度序列值(用 AtomicLong 實現)

注意:period 大于 0 或 小于 0 時,都是周期性執行的,只是執行時間規律不一樣。

ScheduledFutureTask 的主要調度輔助方法如下:

// 任務的延遲執行時間
public long getDelay(TimeUnit unit) {
    return unit.convert(time - now(), NANOSECONDS);
}
//實現任務的排序,執行時間越小越靠前,相同則按照隊列FIFO順序
public int compareTo(Delayed other) {
    if (other == this) // compare zero if same object
        return 0;
    if (other instanceof ScheduledFutureTask) {
        ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
        long diff = time - x.time;
        if (diff < 0)
            return -1;
        else if (diff > 0)
            return 1;
        else if (sequenceNumber < x.sequenceNumber) // 時間一樣時,按照FIFO的順序
            return -1;
        else
            return 1;
    }
    long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
    return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
}

// 是否是周期性任務
public boolean isPeriodic() {
    return period != 0;
}
// 設置下一次運行時間
private void setNextRunTime() {
    long p = period;
    if (p > 0)
        time += p; // 按固定時間周期,下次執行時間為上次執行時間 + 周期時間
    else
        time = triggerTime(-p); // 按固定延時周期,下次執行時間為當前時間 + 延時時間
}

核心 run 方法

public void run() {
    boolean periodic = isPeriodic();
    if (!canRunInCurrentRunState(periodic)) // 判斷是否可以運行任務
        cancel(false);  // 取消任務,移除隊列
    else if (!periodic) // 非周期性任務 直接調用父類 FutureTask 的 run 方法
        ScheduledFutureTask.super.run();
    else if (ScheduledFutureTask.super.runAndReset()) {  // 周期性任務,調用父類 runAndReset 方法,返回是否執行成功
        // 執行成功后繼續設置下一次運行時間
        setNextRunTime(); 
        // 重新執行周期性任務(可能因為線程池運行狀態的改變而被拒絕)
        reExecutePeriodic(outerTask);
    }
}

對于周期性任務,在 run 方法中執行成功后會繼續設置下一次執行時間,并把任務加入延時隊列。但需注意,如果任務執行失敗,將不會再被周期性調用。所以在可能執行失敗的周期性任務中,必須做好異常處理。

DelayedWorkQueue

DelayedWorkQueue 是一個延時有序隊列,內部采用 數組 維護隊列元素,采用 堆排序 的思想維護隊列順序,并在隊列元素(ScheduledFutureTask)建立索引,支持快速刪除。

注意:DelayedWorkQueue 的整個隊列不是完全有序的,只保證元素有序出隊。

DelayedWorkQueue類圖

下面詳細講解 DelayedWorkQueue 的實現:

核心入隊方法:

public boolean add(Runnable e) {
      return offer(e);
}

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(); // 隊列擴容 類似 ArrayList 擴容
        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;
}

入隊方法中最重要的是 siftUp 方法, sift 在英文單詞中是 的意思,這里可將 siftUp 理解為向前篩,找到合適的 堆排序點 加進去。

private void siftUp(int k, RunnableScheduledFuture<?> key) {
    while (k > 0) {
        int parent = (k - 1) >>> 1; // (k-1)/2
        RunnableScheduledFuture<?> e = queue[parent];
        if (key.compareTo(e) >= 0)
            break;
        queue[k] = e;
        setIndex(e, k);
        k = parent;
    }
    queue[k] = key;
    setIndex(key, k);
}

siftUp 主要思想是將新增的任務與前 (k-1)/2 的位置比較,如果任務執行時間較近者替換位置 (k-1)/2。依次往前比較,直到無替換發生。每次新增元素調用 siftUp 僅能保證第一個元素是最小的。整個隊列不一定有序:

例將:5 10 9 3 依次入隊,隊列變化如下
 [5]
 [5,10]
 [5,9,10]
 [3,5,10,9] 

如果對上述的入隊方式不了解,可用下面的排序代碼進行斷點調試:

// DelayedWorkQueue 的入隊、出隊排序模擬
public class SortArray {
    Integer[] queue = new Integer[16];

    int size = 0;

    public static void main(String[] args) {
        SortArray array = new SortArray();
        array.add(5);
        array.add(9);
        array.add(10);
        array.add(3);
        System.out.println(array.take());
        System.out.println(array.take());
        System.out.println(array.take());
        System.out.println(array.take());
    }

    boolean add(Integer e) {
        if (e == null)
            throw new NullPointerException();
        int i = size;
        size = i + 1;
        if (i == 0) {
            queue[0] = e;
        } else {
            siftUp(i, e);
        }
        return true;
    }
    
    Integer take() {
        Integer i = queue[0];
        int s = --size;
        Integer k = queue[s];
        if (size != 0)
            siftDown(0, k);
        return i;
    }

    private void siftUp(int k, Integer key) {
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Integer e = queue[parent];
            if (key.compareTo(e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }
    
     private void siftDown(int k, Integer key) {
         int half = size >>> 1;
         while (k < half) {
             int child = (k << 1) + 1;
             Integer c = queue[child];
             int right = child + 1;
             if (right < size && c.compareTo(queue[right]) > 0)
                 c = queue[child = right];
             if (key.compareTo(c) <= 0)
                 break;
             queue[k] = c;
             k = child;
         }
         queue[k] = key;
     }
}

核心出隊方法:

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; 
                // 看是否有正在等待的leader線程
                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();
    }
}

代碼中的 available 是一個信號量,會在隊列的頭部有新任務變為可用或者新線程可能需要成為領導者時,發出信號。

private final Condition available = lock.newCondition();

take() 方法中重要的方法是 finishPoll(first) ,主要進行出隊時維護隊列順序:

private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
    int s = --size;
    RunnableScheduledFuture<?> x = queue[s];
    queue[s] = null;
    if (s != 0)
        siftDown(0, x);
    setIndex(f, -1);
    return f;
}

private void siftDown(int k, RunnableScheduledFuture<?> key) {
    int half = size >>> 1;
    while (k < half) {
        int child = (k << 1) + 1;
        RunnableScheduledFuture<?> c = queue[child];
        int right = child + 1;
        if (right < size && c.compareTo(queue[right]) > 0)
            c = queue[child = right];
        if (key.compareTo(c) <= 0)
            break;
        queue[k] = c;
        setIndex(c, k);
        k = child;
    }
    queue[k] = key;
    setIndex(key, k);
}

siftDown 跟前面的 siftUp 很像,它也只能保證出隊后下一個仍為最近的任務。并不會移動和清理整個隊列。

還是用上面列出的 SortArray 這個類為例:

    public static void main(String[] args) {
        SortArray array = new SortArray();
        array.add(5);
        array.add(9);
        array.add(10);
        array.add(3);
        System.out.println(Arrays.toString(array.queue));
        System.out.println(array.take());
        System.out.println(array.take());
        System.out.println(array.take());
        System.out.println(array.take());
        System.out.println(Arrays.toString(array.queue));
        array.add(20);
        array.add(4);
        System.out.println(Arrays.toString(array.queue));
    }

我們先將5,9,10,3 依次入隊,然后全部出隊,再入隊 20,4,我們看下最后的隊列里面的數據是什么樣子:

[3, 5, 10, 9, null, null, null, null, null, null, null, null, null, null, null, null]
3
5
9
10
[10, 10, 10, 9, null, null, null, null, null, null, null, null, null, null, null, null]
[4, 20, 10, 9, null, null, null, null, null, null, null, null, null, null, null, null]

看了這個結果你可能有點奇怪,已經出隊了的元素居然還在隊列里面。這是一種 lazy 策略,DelayedWorkQueue 并不會真正直接清理掉隊列里出隊的元素,用 size 來控制隊列的邏輯大小,并發物理實際大小,后來的元素會根據size來覆蓋原有的元素。

關于 DelayedWorkQueue 的出隊和入隊還有疑問的,可以自己調試 SortArray 的代碼,看看不同的情況的不同處理結果。DelayedWorkQueue 的 siftUp 、siftDown 這種排序策略非常高效,并非維護整個隊列實時有序,只保證第一個出隊元素的正確性。

元素刪除

上文有提到 ScheduledFutureTask 的索引,DelayedWorkQueue 運用索引可以快速定位刪除元素:

public boolean remove(Object x) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        int i = indexOf(x);
        if (i < 0)
            return false;

        setIndex(queue[i], -1);
        int s = --size;
        RunnableScheduledFuture<?> replacement = queue[s];
        queue[s] = null;
        if (s != i) {
            siftDown(i, replacement); // 順序調整
            if (queue[i] == replacement)
                siftUp(i, replacement);
        }
        return true;
    } finally {
        lock.unlock();
    }
}

// 使用索引獲取下標
private int indexOf(Object x) {
    if (x != null) {
        if (x instanceof ScheduledFutureTask) {
            int i = ((ScheduledFutureTask) x).heapIndex; // 索引
            if (i >= 0 && i < size && queue[i] == x)
                return i;
        } else {
            for (int i = 0; i < size; i++)
                if (x.equals(queue[i]))
                    return i;
        }
    }
    return -1;
}

remove方法里面首先利用 indexOf 調用索引獲取下標,然后使用 siftDownsiftUp 來調整隊列順序。這里索引的使用能夠極大提高元素定位的效率,尤其是在隊列比較長的時候。

最后思考一個問題:為什么 DelayedWorkQueue 使用數組而不是鏈表結構?

個人認為,因為使用數據結構,利用下標快速訪問,可以發揮基于 siftDown,siftUp 的高效排序算法,而鏈表的下標訪問效率低,因此選擇使用數組。

多線程系列目錄(不斷更新中):
線程啟動原理
線程中斷機制
多線程實現方式
FutureTask實現原理
線程池之ThreadPoolExecutor概述
線程池之ThreadPoolExecutor使用
線程池之ThreadPoolExecutor狀態控制
線程池之ThreadPoolExecutor執行原理
線程池之ScheduledThreadPoolExecutor概述
線程池之ScheduledThreadPoolExecutor調度原理
線程池的優雅關閉實踐

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。