Java隊列之DelayQueue源碼解析

目錄

1.DelayQueue

1.1整體結構

1.2放數據

1.3拿數據


1.DelayQueue


1.1整體結構

  • 延遲執行,並且可以設置延遲多久之後執行
  • 隊列中元素將在過期時被執行,越靠近隊頭,越早過期
  • 底層使用了優先級隊列來實現,複用組合了PriorityQueue(策略者模式),讓先過期的元素先執行
//隊列的元素必須爲Delayed本身或者子類
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {
    ...
    private final PriorityQueue<E> q = new PriorityQueue<E>();
    // 只有一把鎖,讀和寫同一時刻只能有一個在運行
    private transient final ReentrantLock lock = new ReentrantLock();
    ...
}
//繼承了Comparable接口,用來比較大小
public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}
  • DelayQueue 隊列中的元素必須是實現 Delayed 接口和 Comparable 接口的,並覆寫了 getDelay 方法和 compareTo 的方法纔行,不然在編譯時,編譯器就會提醒我們元素必須強制實現 Delayed 接口。

ps:只有一把鎖,讀和寫同一時刻只能有一個在運行

1.2放數據

public boolean offer(E e) {
    final ReentrantLock lock = this.lock; //會鎖住當前整個實例
    // 上鎖
    lock.lock();
    try {
        // 使用 PriorityQueue 的擴容,排序
        q.offer(e);
        // 如果恰好剛放進去的元素正好在隊列頭,需要喚醒 take 的阻塞線程
        // 空隊列一定也會進入判斷,因爲一定是隊列頭!!!
        if (q.peek() == e) {
            leader = null;
            available.signal();
        }
        return true;
    } finally {
        // 釋放鎖
        lock.unlock();
    }
}

DelayQueue的offer方法核心塊使用了PriorityQueue的offer方法

加鎖,使用PriorityQueue的offer方法判斷擴容並且按序插入,如果元素恰好放在隊列頭就喚醒一個take線程

// PriorityQueue的offer方法:新增元素
public boolean offer(E e) {
    // 如果是空元素的話,拋異常
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    // 隊列實際大小大於容量時,進行擴容
    // 擴容策略是:如果老容量小於 64,2 倍擴容,如果大於 64,50 % 擴容
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    // 如果隊列爲空,當前元素正好處於隊頭
    if (i == 0)
        queue[0] = e;
    else
    // 如果隊列不爲空,插入堆,從下向上冒泡
        siftUp(i, e);
    return true;
}
// 維護一個最小堆!!!
// 新增操作:從下向上冒泡的過程不需要左右節點在比較的!!!
// 刪除操作:從上向下冒泡的過程是需要左右節點比較然後在進行交換的!!!
    private void siftUp(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        // k 是當前隊列實際大小的位置
        while (k > 0) {
            // 對 k 進行減倍
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            // 如果 x 比 e 大,退出,把 x 放在 k 位置上
            if (key.compareTo((E) e) >= 0)
                break;
            // x 比 e 小,繼續循環,直到找到 x 比隊列中元素大的位置
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }

PriorityQueue的offer方法:先判斷是否需要擴容(如果元素個數少於64每次擴容2倍,否則每次擴容1.5倍),如果隊列爲空那麼直接在頭節點新增即可,如果不爲空,就需要進行堆插入從下向上進行冒泡,不斷的跟父節點進行比較即可,最後返回true新增成功!

ps:因爲本身就是一個堆,所以不需要在判斷另一個節點的值,這一點跟從上向下冒泡不同,後者需要比較出左右節點的最值!

ps:堆排序首先需要維護一個堆(從下向上對每個非葉子節點自上向下冒泡維護一個最大堆),然後進行實際的排序(每次與堆尾部交換頭節點,然後從堆頭自上向下維護堆結構)

1.3拿數據

//take方法:無限阻塞
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock; //獲取鎖,會鎖住整個實例
    lock.lockInterruptibly();// 可中斷鎖
    for (;;) {
        // 從隊頭中拿數據出來
        E first = q.peek();
        // 如果爲空,說明隊列中,沒有數據,阻塞住
        if (first == null)
            available.await();
        else {
            // 獲取隊頭數據的過期時間
            long delay = first.getDelay(NANOSECONDS);
            // 如果過期了,直接返回隊頭數據
            if (delay <= 0)
                return q.poll();
            // 引用置爲 null ,便於 gc,這樣可以讓線程等待時,回收 first 變量
            first = null;
            // leader 不爲空的話,表示當前隊列元素之前已經被設置過阻塞時間了
            // 已經被設置阻塞時間不會進入這裏吧!!!!finally不是保證重置了嗎???
            if (leader != null)
                available.await();
            else {
              // 之前沒有設置過阻塞時間,按照一定的時間進行阻塞
                Thread thisThread = Thread.currentThread();
                leader = thisThread;
                try {
                    // 進行阻塞,等待delay延遲時間
                    available.awaitNanos(delay);
                } finally {//一定會重置
                    if (leader == thisThread)
                        leader = null;
                }
            }
    }finally {
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
    }
}

加鎖並且設置爲可中斷,如果隊列爲空就會阻塞,如果不爲空會先獲取延遲時間,如果延時過期(可以執行了)就刪除頭節點並且執行,如果沒有設置阻塞時間會進行延時等待

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