JUC源碼解析-阻塞隊列-DelayQueue

DelayQueue是一個支持延時獲取元素的無界阻塞隊列。隊列使用PriorityQueue來實現。隊列中的元素必須實現Delayed接口,在創建元素時可以指定多久才能從隊列中獲取當前元素。只有在延遲期滿時才能從隊列中提取元素。我們可以將DelayQueue運用在以下應用場景:

  • 緩存系統的設計:可以用DelayQueue保存緩存元素的有效期,使用一個線程循環查詢DelayQueue,一旦能從DelayQueue中獲取元素時,表示緩存有效期到了。
  • 定時任務調度。使用DelayQueue保存當天將會執行的任務和執行時間,一旦從DelayQueue中獲取到任務就開始執行,從比如TimerQueue就是使用DelayQueue實現的。

隊列中的Delayed必須實現compareTo來指定元素的順序。比如讓延時時間最長的放在隊列的末尾。

關於使用:java併發之DelayQueue實際運用示例

關於其底層依賴的優先級隊列PriorityQueue 其實就是堆算法,在上一篇 PriorityBlockingQueue 中分析過該算法,關於堆算法推薦 排序六 堆排序

Delayed

public interface Delayed extends Comparable<Delayed> {

    long getDelay(TimeUnit unit);
}

TimeUnit

public enum TimeUnit {
    /**
     * Time unit representing one thousandth of a microsecond
     */
    NANOSECONDS {
        public long toNanos(long d)   { return d; }
        public long toMicros(long d)  { return d/(C1/C0); }
        public long toMillis(long d)  { return d/(C2/C0); }
        public long toSeconds(long d) { return d/(C3/C0); }
        public long toMinutes(long d) { return d/(C4/C0); }
        public long toHours(long d)   { return d/(C5/C0); }
        public long toDays(long d)    { return d/(C6/C0); }
        public long convert(long d, TimeUnit u) { return u.toNanos(d); }
        int excessNanos(long d, long m) { return (int)(d - (m*C2)); }
    },
    MICROSECONDS
    MILLISECONDS
    SECONDS
    MINUTES
    HOURS
    DAYS
    ......

DelayQueue

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {
	// 鎖
    private final transient ReentrantLock lock = new ReentrantLock();
    // 底層以理優先級隊列 PriorityQueue
    private final PriorityQueue<E> q = new PriorityQueue<E>();

	/**
	代表等待隊列頭元素的線程。這種Leader-Follower模式的變體用於最小化不必要的定時等待。
	當一個線程成爲領頭線程時,它的等待時間即是當時堆頂元素距到期的時間,而其他線程則無限期地等待。
	在從take()或poll(…)返回之前,會發出signal信號給等待的線程,除非有其他線程在等待期間成爲leader線程。
	每當隊列的頭替換爲具有較早到期時間的元素時,leader字段將被重置爲空值而無效,並且會發出signal信號給等待線程。
	因此,等待線程甦醒後可能會成爲leader,具體看下面代碼的分析
	*/
    private Thread leader = null;

     // 等待線程在兩處被 signal : 1,新的堆頂元素出現。2,leader爲null,需要一個新的leader線程時
    private final Condition available = lock.newCondition();

延遲隊列,它的每個對象都有自己的到期時間,按照你定的比較規則構成一個最小堆,堆頂是優先級最大的對象。

如果你的比較規則是到期時間,那麼堆頂就是最快到期的,如果不是那麼堆頂就可能不是最快到期的,也就是堆裏可能存在已到期的對象。

Leader-Follower 模式,來看看其實現主體 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; // 置爲null,防止線程等待期間一直只有該對象
                    if (leader != null) // 已有leader線程,等待阻塞
                        available.await();
                    else {
                    	// 將當前線程置爲 leader,等待直到堆頂到期
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        try {
                            available.awaitNanos(delay);
                        } finally { // 當leader恢復執行,若leader仍是該線程則置空
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
        	// leader 爲null,選取下一個 leader 。
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
        }
    }

該方法實現了 leader 線程的等待,新 leader 線程的選取,堆頂元素的獲取操作。

Leader-Follower 模式是爲了儘可能縮短不必要的等待時間,如何做到的 ?

1,leader 線程只阻塞一定的時間,awaitNanos(delay),delay 爲當時的堆頂元素距離到期的時間,併發情況是複雜的,堆頂可能時常在變,那麼 leader 的目的又在哪?leader 確保當堆頂到期後一定會有線程來取它,那麼過時的 leader 是浪費嗎?不是,堆頂到期的leader 並非立刻執行,它首先是回到AQS的同步隊列中排對等待,那麼在這段等待時間,那些過時的 leader 線程的節點可能穿插在其中,先一步被喚醒就能提前將堆頂取出,這樣就節約了時間。

2,ReentrantLock 是非公平模式,也就是新線程可以插隊。比如堆頂到期了,其 leader 線程節點回到 AQS 的同步隊列中等待被喚醒執行,可能需要一段時間,由於允許插隊則新的取線程可能插隊成功,在leader 線程之前取出堆頂,也就節約了堆頂的等待時間。

來分析下 leader 線程恢復執行會面對哪些情況?

leader 線程並非是爲了取出當初與它對應的那個堆頂元素設計的,我們看上面的代碼,當leader 線程甦醒,首先檢查 leader 字段是否仍指向自己(offer 方法若更新了堆頂會將leader字段指控,下面再分析),若是則置空,爲什麼?爲了給新堆頂選出 leader 線程,也就確保了新堆頂到期會有取線程來取它。

併發下情況是複雜的,甦醒的 leader 線程會遭遇那些情況呢?

1)堆頂仍是當初那個沒變,取出,退出循環來到 finally 塊,發出 signal 信號,下一個 leader 可能是插隊的新線程或是被信號喚醒的線程。
2)leader 仍指向該線程,不過堆頂已改變,原因是 leader 在同步隊列等待過程中被新的取線程插隊,取走了原堆頂,這也沒什麼關係,上面說了 leader 設計並非是與特定堆頂綁定,當其甦醒你可以將它當作一個普通的取操作線程:查看堆頂能否取出,能就取出然後發出signal 信號,之後執行的線程會成爲新堆頂的 leader,當然前提是堆頂未到期;若是不能取出就看新堆頂有無 leader,有則當前線程 await 等待,否則當前線程成爲新堆頂的 leader。
3)堆頂 ,leader 都已改變,因爲 offer 方法插入了新堆頂,他會置空 leader 字段併發出 signal 信號,爲什麼?爲了儘快爲新堆頂選出 leader 從而保證當其到期時有線程來取它。

offer

    public boolean offer(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            q.offer(e);
            // 若原先數組爲空或新插入的 e 優先級最高成爲堆頂
            // leader置爲null,發出signal信號。爲什麼?
            //爲了儘快爲新堆頂選出 leader 從而保證當其到期時有線程來取它
            if (q.peek() == e) {
                leader = null;
                available.signal();
            }
            return true;
        } finally {
            lock.unlock();
        }
    }

add,put

都是調用的 offer

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

    public void put(E e) {
        offer(e);
    }

poll

poll 操作是不阻塞的,它首先探測堆頂元素,若其到期則彈出堆頂,否則返回null

    public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            E first = q.peek();
            if (first == null || first.getDelay(NANOSECONDS) > 0)
                return null;
            else
                return q.poll();
        } finally {
            lock.unlock();
        }
    }

remove

不管是否到期都刪除它。

    public boolean remove(Object o) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return q.remove(o);
        } finally {
            lock.unlock();
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章