深入理解 JUC:DelayQueue

延遲隊列 DelayQueue 用於存放具有過期屬性的元素,被添加到 DelayQueue 中的元素只有在到達過期時間之後纔會出隊列,常用於延遲任務調度。DelayQueue 本質上是一個無界的阻塞隊列,底層依賴於優先級隊列 PriorityQueue 作爲存儲結構,並使用 ReentrantLock 鎖保證線程安全。

DelayQueue 的字段定義如下:

public class DelayQueue<E extends Delayed> extends AbstractQueue<E> implements BlockingQueue<E> {

    /** 保證線程安全的可重入獨佔鎖 */
    private final transient ReentrantLock lock = new ReentrantLock();

    /** 底層存儲結構,優先級隊列 */
    private final PriorityQueue<E> q = new PriorityQueue<E>();

    /** Leader-Follower 模式,用於記錄角色爲 leader 的線程對象 */
    private Thread leader = null;

    /** 因爲隊列爲空,或元素未到期而阻塞的線程 */
    private final Condition available = lock.newCondition();

    // ... 省略方法定義

}

重點分析一下 DelayQueue#leader 字段設置的意圖,該字段用於記錄當前角色爲 leader 的線程對象。當執行出隊列操作(DelayQueue#takeDelayQueue#poll(long, TimeUnit) 方法)時,如果 DelayQueue#leader 字段爲 null,即不存在 leader 線程,且有未到期的延遲元素,則會將當前線程設置成 leader 角色,並等待該元素到期,以減少不必要的等待時間,保證延遲元素能夠在到期時及時被響應。

設想如果不這樣設計,那麼線程在遇到隊列中沒有到期的延遲元素時應該怎麼辦呢?可以採取以下 3 種策略:

  1. 進入忙循環輪詢。
  2. 進入條件隊列等待,並稍後由其它線程喚醒。
  3. 先退出當前方法,等待後續再次執行出隊列操作。

可以看出,這些策略要麼是消耗 CPU 資源,要麼就是無法對隊列中的元素在到期時及時出隊列,而引入 leader 角色正好能夠避免了這些問題。在某個線程以 leader 的身份等待優先級最高的延遲元素到期時,其它線程在發現隊列中沒有到期的元素時會以 follower 角色無限期等待。而在 leader 線程從等待狀態退出時,它會主動放棄自己的 leader 角色,並喚醒一個正在處於等待狀態的 follower 線程,該線程將有機會晉升成爲新的 leader。

在 leader 線程等待期間,有可能會插入優先級更高的元素,這個時候就需要剝奪該線程的 leader 角色,以提供其它線程成爲 leader 的機會,繼而保證剛剛新插入的元素能夠在到期時及時被響應。否則就需要等到當前 leader 線程退出等待狀態之後或者有新的線程請求獲取元素時纔有機會出隊列,這樣可能存在較大的延遲。

核心方法實現

DelayQueue 實現自 BlockingQueue 接口,下面針對核心方法的實現逐一進行分析。不過在開始分析之前,我們先來介紹一下 java.util.concurrent.Delayed 接口,DelayQueue 要求添加到其中的元素必須實現該接口。Delayed 接口定義如下:

public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}

該接口繼承自 Comparable 接口,所以添加到 DelayQueue 中的元素都是可比較的。方法 Delayed#getDelay 接收一個 TimeUnit 類型的單位值,用於返回當前延遲元素的剩餘到期時間,如果小於等於 0 則說明該元素已經到期。

添加元素:offer & add & put

針對添加元素的操作,DelayQueue 實現了 DelayQueue#offerDelayQueue#addDelayQueue#put 方法,不過後兩者都是直接調用了 DelayQueue#offer 方法。

此外,該方法的超時版本 DelayQueue#offer(E, long, TimeUnit) 也是直接委託給 DelayQueue#offer 方法執行,並沒有真正實現超時等待機制。這主要是因爲 DelayQueue 是無界的,所有的添加操作都能夠被立即響應,而不會阻塞。

下面展開分析一下 DelayQueue#offer 方法的實現,如下:

public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    // 加鎖
    lock.lock();
    try {
        // 往隊列中添加元素
        q.offer(e);
        // 當前添加的元素是隊列中最先過期的
        if (q.peek() == e) {
            // 清空 leader,保證該元素能夠及時出隊列
            leader = null;
            // 喚醒因元素均爲到期或隊列爲空而等待的線程
            available.signal();
        }
        return true;
    } finally {
        // 釋放鎖
        lock.unlock();
    }
}

DelayQueue 不允許向其中添加值爲 null 的元素,這主要由優先級隊列 PriorityQueue 保證。如果待添加的元素值合法,則執行:

  1. 加鎖,保證同一時間只有一個線程在操作隊列;
  2. 將待添加元素插入到 DelayQueue 中;
  3. 檢查 DelayQueue 中優先級最高的的元素是否是剛剛新加入的元素;
  4. 如果是則剝奪當前 leader 線程的 leader 角色,並喚醒一個之前因爲隊列中的元素均未到期或隊列爲空而等待的線程;
  5. 釋放鎖並返回。

爲什麼當隊列中插入了一個優先級最高的元素時需要剝奪當前 leader 線程的 leader 角色,並喚醒一個處於等待的 follower 線程呢?

其實在前面也有所提及,假設現在 DelayQueue 中最先過期的元素還有 10 秒到期,則 leader 線程會等待 10 秒後再次嘗試出隊列,其它 follower 線程因爲檢測到當前已有線程成爲 leader,所以在發現沒有已到期的元素時會等待。假設現在有一個還有 5 秒到期的元素插入了進來,如果在該元素到期之後沒有新的線程來請求出隊列,則該元素將不能及時被響應,直到 leader 線程退出等待,即使此時有相當數量的 follower 線程在等待元素到期。

獲取元素:poll & peek & take

針對獲取元素的操作,DelayQueue 實現了 DelayQueue#pollDelayQueue#peekDelayQueue#take 方法。其中 DelayQueue#peek 方法在獲取到鎖的基礎上直接調用了 PriorityQueue#peek 方法,僅獲取 DelayQueue 中最先到期的元素(獲取時可能還未到期),而不移除該元素,實現上比較簡單。

方法 DelayQueue#take 相對於 DelayQueue#poll 的區別在於,當隊列爲空或沒有到期的元素時該方法會無限期阻塞,直到有元素到期或該線程被中斷,而 DelayQueue#poll 方法在相同場景下則會立即返回 null。

下面分別展開分析這兩個方法的實現,首先來看一下 DelayQueue#poll 方法,實現如下:

public E poll() {
    final ReentrantLock lock = this.lock;
    // 獲取鎖
    lock.lock();
    try {
        // 獲取隊列中最先過期的元素
        E first = q.peek();
        // 隊列爲空,或者元素還未到期,立即返回 null
        if (first == null || first.getDelay(NANOSECONDS) > 0) {
            return null;
        }
        // 當前元素已過期,移除並返回
        else {
            return q.poll();
        }
    } finally {
        // 釋放鎖
        lock.unlock();
    }
}

上述方法會檢查 DelayQueue 中是否有已經到期的元素,如果有則將該元素出隊列並返回,否則,如果隊列爲空或沒有已經到期的元素,則立即返回 null。針對 DelayQueue#poll 方法,DelayQueue 還提供了超時版本 DelayQueue#poll(long, TimeUnit),當隊列爲空或沒有已經到期的元素時等待指定時間。

再來看一下 DelayQueue#take 方法的實現,如下:

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    // 獲取鎖,支持響應中斷
    lock.lockInterruptibly();
    try {
        for (; ; ) {
            // 獲取最先過期的元素
            E first = q.peek();
            // 隊列爲空,則等待
            if (first == null) {
                available.await();
            }
            // 隊列非空
            else {
                // 如果當前元素已經過期,則出隊列
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0) {
                    return q.poll();
                }

                /* 當前元素還未過期 */

                first = null; // don't retain ref while waiting

                // 已經有其它線程成爲 leader,則等待
                if (leader != null) {
                    available.await();
                }
                // 沒有 leader 線程,將自己設置爲 leader 角色
                else {
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        // 等待 delay 納秒
                        available.awaitNanos(delay);
                    } finally {
                        // 如果等待期間自己的 leader 角色未被剝奪,則在等待完成之後主動放棄
                        if (leader == thisThread) {
                            leader = null;
                        }
                    }
                }
            }
        }
    } finally {
        // 如果隊列不爲空,則喚醒一個之前因爲隊列爲空而等待的線程
        if (leader == null && q.peek() != null) {
            available.signal();
        }
        // 釋放鎖
        lock.unlock();
    }
}

方法 DelayQueue#take 在獲取到鎖之後會先檢查隊列是否爲空,如果爲空則等待,否則執行:

  1. 如果 DelayQueue 中優先級最高的元素已經到期,則出隊列並返回該元素;
  2. 否則,如果當前已經有 leader 線程,則等待;
  3. 如果當前沒有 leader 線程,則將自己設置爲 leader 角色,並等待隊列中優先級最高的元素到期。

如果 DelayQueue 中優先級最高的元素到期,或者等待期間被中斷,則當前 leader 線程會主動放棄自己的 leader 角色,以給其它 follower 線程機會。當然在等待期間,當前線程的 leader 角色也可能會被剝奪,前面我們在分析 DelayQueue#offer 方法時已經介紹過,當等待期間有其它優先級更高的元素插入進來時,執行插入的線程會剝奪當前 leader 線程的 leader 角色,以便讓剛剛插入的優先級更高的元素能夠在到期時及時出隊列。

在整個 DelayQueue#take 方法執行的最後,如果 DelayQueue 非空,且當前沒有線程成爲 leader,則會喚醒一個之前因爲隊列爲空而阻塞的 follower 線程。這裏限制 leader == null 主要是防止在有 leader 存在的前提下,被喚醒的線程會因爲隊列中沒有到期的元素而再次等待。

移除元素:remove

針對移除元素的操作,DelayQueue 實現了 DelayQueue#remove 方法,並提供了有參和無參的版本,其中無參版本實際上是委託給 DelayQueue#poll 方法執行的。下面來分析一下有參版本的實現,如下:

public boolean remove(Object o) {
    final ReentrantLock lock = this.lock;
    // 加鎖
    lock.lock();
    try {
        // 從優先級隊列中移除元素
        return q.remove(o);
    } finally {
        // 釋放鎖
        lock.unlock();
    }
}

DelayQueue 移除指定元素的操作在獲取到鎖的前提下,交由 PriorityQueue 執行,實現上比較簡單。

DelayQueue 的 DelayQueue#size 方法在實現上與 DelayQueue#remove 方法思路相同,不再展開。但需要注意的一點是,方法 DelayQueue#size 所返回的值並不僅僅包含那些未到期的元素,也可能包含一些已經到期而未被從隊列中移除的元素,一種可能是這些元素未被及時響應,另外一種可能就是線程僅獲取了元素值,而沒有移除對應的結點,例如調用了 DelayQueue#peek 方法。

總結

本文分析了 DelayQueue 的設計與實現。DelayQueue 相對於前面介紹的隊列的特別之處在於引入了時間屬性,只有在元素到期時纔會被出隊列。在實現上,DelayQueue 底層依賴於優先級隊列 PriorityQueue 作爲存儲結構,並基於 ReentrantLock 鎖保證線程安全,同時巧妙設計了 Leader-Follower 模式來保證延遲元素在到期時能夠及時被響應。

參考

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