PriorityBlockingQueue 筆記

介紹

PriorityBlockingQueue 是基於二叉堆(binary heap)實現的有序阻塞隊列。隊列容量上線是 Integer.MAX_VALUE - 8,如果一直往隊列中添加元素,並且來不及消費,可能會產生 OOM。PriorityBlockingQueue 的排序策略和 PriorityQueue 的排序策略一直,都是基於比較器(Comparator),如果沒有比較機器則基於對象的 compareTo 方法。所以如果沒比較器,那麼必須要求隊列元素實現 Comparator 方法。

PriorityBlockingQueue 繼承自 Collection,也有迭代器相關功能。但是由於本身是基於二叉堆實現,遍歷隊列元素時無法按照排序的順序遍歷。如果需要按照順序的順序遍歷,那麼可以將 PriorityBlockingQueue 先轉成數組(toArray()),在利用 Arrays.sort 將數組排序。

PriorityBlockingQueue 的繼承關係如下:
繼承關係

實現原理

PriorityBlockingQueue 利用二叉堆的數據結構存儲元素,能夠保證隊首或者最大(最大堆)或者最小(最小堆)來實現 PriorityBlockingQueue 每次出隊元素最大或最小保證出隊能夠按照優先級。PriorityBlockingQueue 提供按照排序獲取元素,但是並不要求隊列內部的元素都按照這個順序存放,所以二叉堆是一個非常好的選擇。二叉堆既能夠保證堆頂最大或者最小,又沒有爲了保證整體的順序而帶來的自旋等額外操作。

PriorityBlockingQueue 是基於數組實現的二叉堆,堆頂元素是數組下標 0。如果某元素的下標是 n,那麼左子元素的下標是 2n+1,右子元素下標是 2(n+1)。如下是元素對應的二叉堆和數組的示意圖:
二叉堆與數組

二叉堆新元素入隊:

  1. 將新元素添加到數組上有效元素的後一個位置上。
  2. 將新元素和父元素比較大小,如果有必要則調整父元素和新元素的位置。
  3. 如果 2 做調整,那麼繼續比較直到和 0 下標的位置比較。

二叉堆出隊:

  1. 將數組最後一個位置上的元素替換掉 0 位置上的元素,並且最後一個位置置爲 null
  2. 數組有效元素減一
  3. 和左右子節點比較大小,如果需要替換則替換元素位置
  4. 如果 3 做了替換那麼繼續向下比較直到最後一層節點。

爲了保證線程安全,PriorityBlockingQueue 改變隊列數據(數組數據)的操作都是用可重入鎖 ReentrantLock 保護。每次操作前 ReentrantLock 上鎖,操作後釋放鎖。

和其他的阻塞隊列一樣,PriorityBlockingQueue 提供如果隊列滿了,入隊線程阻塞;如果隊列空了,出隊線程阻塞的功能。PriorityBlockingQueue 利用 AbstractQueuedSynchronizer 提供的 ConnectionObject 的等待隊列實現線程不滿足隊列要求而進入等待隊列直到阻塞隊列滿足要求後通知線程繼續執行。

初始化方法

PriorityBlockingQueue 提供三個構造方法:

  1. 無參構造函數:利用默認值構建數組大小並且不指定比較策略。需要元素本省能夠支持比較(繼承 Comparable)。
  2. 指定初始構建數組大小和指定比較策略構方法
  3. 根據指定集合構建阻塞隊列的構造方法

具體代碼如下:

 /**
     * PriorityBlockingQueue 無參構造函數
     * 初始化一個數組長度爲 11 的數組用於存儲底層元素
     * 由於沒有指定元素的比較策略,所以要求元素本身定義了比較方式。
     *
     */
    public PriorityBlockingQueue() {
        this(DEFAULT_INITIAL_CAPACITY, null);
    }
    
    /**
     * 構造方法
     * 根據指定的容量大小構造一個該大小長度的數組作爲存儲元素的容器(二叉堆的容器)
     * 由於沒有指定元素的比較策略,所以要求元素本身定義了比較方式。
     * @throws IllegalArgumentException if {@code initialCapacity} is less
     *         than 1
     */
    public PriorityBlockingQueue(int initialCapacity) {
        this(initialCapacity, null);
    }
    
    /**
     * 構造方法
     * 根據指定的容量大小構造一個該大小長度的數組作爲存儲元素的容器(二叉堆的容器)
     * 根據指定的排序策略構建二叉堆
     * @param initialCapacity 初始數組大小
     * @param comparator 排序策略
     * @throws IllegalArgumentException 如果 initialCapacity<1 則拋出 IllegalArgumentException 異常
     */
    public PriorityBlockingQueue(int initialCapacity,
                                 Comparator<? super E> comparator) {
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.lock = new ReentrantLock();
        this.notEmpty = lock.newCondition();
        this.comparator = comparator;
        this.queue = new Object[initialCapacity];
    }
    
    /**
     * 構造方法
     * 根據給定的集合構造一個 PriorityBlockingQueue
     *
     * @param  c 集合中的元素將會被添加到新創建到的 PriorityBlockingQueue 中
     * @throws ClassCastException 如果元素沒有提供和其他元素的比較策略那麼拋出 ClassCastException 異常
     * @throws NullPointerException 如果被添加的元素指向 null,那麼拋出該異常
     */
    public PriorityBlockingQueue(Collection<? extends E> c) {
        this.lock = new ReentrantLock();
        this.notEmpty = lock.newCondition();
        // 是否需要根據二叉堆的排序規則重新排序
        boolean heapify = true;
        boolean screen = true;
        /*
         * 如果是排序的數組,那麼將當前的 PriorityBlockingQueue 的策略置爲排序數組的策略。
         * 由於已經是有序,不需要堆化。
         */
        if (c instanceof SortedSet<?>) {
            SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
            this.comparator = (Comparator<? super E>) ss.comparator();
            heapify = false;
        }
        /*
         * 如果集合是 PriorityBlockingQueue,那麼直接使用該 PriorityBlockingQueue 的比較策略
         * 由於是 PriorityBlockingQueue,不需要堆化
         */
        else if (c instanceof PriorityBlockingQueue<?>) {
            PriorityBlockingQueue<? extends E> pq =
                (PriorityBlockingQueue<? extends E>) c;
            this.comparator = (Comparator<? super E>) pq.comparator();
            screen = false;
            if (pq.getClass() == PriorityBlockingQueue.class) // exact match
                heapify = false;
        }
        Object[] a = c.toArray();
        int n = a.length;
        // 利用數組拷貝,生成底層數組
        if (a.getClass() != Object[].class)
            a = Arrays.copyOf(a, n, Object[].class);
        //PriorityBlockingQueue 不支持入隊 null,所以對生成後的數組做 Null 檢查。
        if (screen && (n == 1 || this.comparator != null)) {
            for (int i = 0; i < n; ++i)
                if (a[i] == null)
                    throw new NullPointerException();
        }
        this.queue = a;
        this.size = n;
        // 如果需要堆化,則執行堆化。堆化是指按照二叉堆的排序規則重新對數組排序
        if (heapify)
            heapify();
    }

出隊方法

PriorityBlockingQueue 提供了三種元素出隊方法和一中獲取隊列第一個元素的方法。
poll(): 如果隊列是空隊列,則返回 null
take(): 如果隊列是空隊列,則線程阻塞直到非空信號通知到該阻塞線程
poll(long timeout, TimeUnit unit): 如果隊列是控隊列,那麼線程阻塞。但是可以設置等待超時時間
peek(): 只返回隊首元素,不改變隊列。

    public E poll() {
          final ReentrantLock lock = this.lock;
          lock.lock();
          try {
              // 利用 dequeue 方法將隊首元素出隊,並將隊首元素返回給調用者
              return dequeue();
          } finally {
              lock.unlock();
          }
      }
    
      public E take() throws InterruptedException {
          final ReentrantLock lock = this.lock;
          lock.lockInterruptibly();
          E result;
          try {
              // 利用 dequeue 方法將隊首元素出隊,但是如果返回的是 null,說明本次出隊時隊列是空隊列,線程進入等待隊列。
              while ( (result = dequeue()) == null)
                  notEmpty.await();
          } finally {
              lock.unlock();
          }
          return result;
      }
    
      public E poll(long timeout, TimeUnit unit) throws InterruptedException {
          long nanos = unit.toNanos(timeout);
          final ReentrantLock lock = this.lock;
          lock.lockInterruptibly();
          E result;
          try {
              // 利用 dequeue 方法將隊首元素出隊,但是如果返回的是 null,說明本次出隊時隊列是空隊列,線程進入等待隊列,直到非空隊列信號通知到該線程或者超過等待時間
              while ( (result = dequeue()) == null && nanos > 0)
                  nanos = notEmpty.awaitNanos(nanos);
          } finally {
              lock.unlock();
          }
          return result;
      }
    
      public E peek() {
          final ReentrantLock lock = this.lock;
          lock.lock();
          try {
              // 如果隊列是空隊列,則返回 Null,非空隊列則返回隊首元素
              return (size == 0) ? null : (E) queue[0];
          } finally {
              lock.unlock();
          }
      }
    
      /**
       * 私有的出隊,爲公有的出隊方法提供出隊操作
       */
      private E dequeue() {
          // 數組最後一個元素的下標位置
          int n = size - 1;
          // 空隊列,返回 null
          if (n < 0)
              return null;
          else {
              Object[] array = queue;
              // 數組的一個元素時隊首元素,該元素出隊
              E result = (E) array[0];
              // 二叉堆出隊:將數組最後一個元素移到下標 0 的位置,然後做下浮操作
              E x = (E) array[n];
              array[n] = null;
              Comparator<? super E> cmp = comparator;
              // 二叉堆下浮
              if (cmp == null)
                  siftDownComparable(0, x, array, n);
              else
                  siftDownUsingComparator(0, x, array, n, cmp);
              // 隊列大小減一
              size = n;
              return result;
          }
      }

入隊方法

PriorityBlockingQueue 提供了 4 個入隊方法,分別是:

  1. add(E e):如果添加成功返回 true,否則返回 false。方法內部直接調用 offer(E e)實現。
  2. offer(E e):入隊方法。如果入隊成功則返回 true,否則返回 false。主要包括三部分內容:
    1. 判斷是否超過數組容量限制,組底層數組擴展
    2. 元素入二叉堆
    3. 發送隊列非空信號
  3. put(E e):元素入隊。直接調用 offer(E e),只是不返回添加操作的是否成功
  4. offer(E e, long timeout, TimeUnit unit):直接調用 offer(E e)實現。timeout 參數和 unit 參數直接過濾
    /**
     * 元素入隊。
     * 直接調用 offer(e)實現
     *
     * @param e 入隊元素
     * @return 如果入隊成功返回 true,否則返回 false
     * @throws ClassCastException 如果阻塞隊列沒有指定排序策略,那麼需要依賴元素本身能夠支持比較。如果不支持則拋出 ClassCastException
     * @throws NullPointerException 如果添加的元素時 Null 那麼拋出 NullPointerException
     */
    public boolean add(E e) {
        return offer(e);
    }
    
    /**
     * 元素入隊
     * 判斷是否超過數組容量限制,組底層數組擴展
     * 元素入二叉堆
     * 發送隊列非空信號
     *
     * @param e 入隊元素
     * @return 如果入隊成功返回 true,否則返回 false
     * @throws ClassCastException  如果阻塞隊列沒有指定排序策略,那麼需要依賴元素本身能夠支持比較。如果不支持則拋出 ClassCastException
     * @throws NullPointerException 如果添加的元素時 Null 那麼拋出 NullPointerException
     */
    public boolean offer(E e) {
        // 如果隊列元素時空對象那麼拋出 NullPointerException 異常
        if (e == null)
            throw new NullPointerException();
        final ReentrantLock lock = this.lock;
        lock.lock();
        int n, cap;
        Object[] array;
        // 如果超過了容量上限,那麼先將底層數組擴容
        while ((n = size) >= (cap = (array = queue).length))
            tryGrow(array, cap);
        try {
            Comparator<? super E> cmp = comparator;
            // 如果阻塞隊列有比較策略,那麼使用比較策略比較元素大小(確定在二叉堆中的位置),如果沒有則使用元素本身的比較策略。
            // 將元素添加到 n 位置(數組後效元素位置+1),然後執行二叉堆上浮操作。
            if (cmp == null)
                siftUpComparable(n, e, array);
            else
                siftUpUsingComparator(n, e, array, cmp);
            // 阻塞隊列容量加一
            size = n + 1;
            // 發送隊列非空信號
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
        return true;
    }
    
    /**
     *  元素入隊
     *
     * @param e 入隊元素
     * @return 如果入隊成功返回 true,否則返回 false
     * @throws ClassCastException  如果阻塞隊列沒有指定排序策略,那麼需要依賴元素本身能夠支持比較。如果不支持則拋出 ClassCastException
     * @throws NullPointerException 如果添加的元素時 Null 那麼拋出 NullPointerException
     */
    public void put(E e) {
        offer(e); // never need to block
    }
    
    /**
     * 元素入隊。
     * timeout 和 unit 參數不起任何作用。
     */
    public boolean offer(E e, long timeout, TimeUnit unit) {
        return offer(e); // never need to block
    }

二叉堆相關方法

PriorityBlockingQueue 是通過基於數組的二叉堆數據結構實現的優先隊列功能,主要的方法包括:

  1. 擴大數組大小
  2. 堆下浮:在堆結構中向下比較(子節點)找到合適的位置(如果存在的話)進行位置轉換
  3. 堆上浮:在堆結構中向上比較(父節點)找到合適的位置(如果存在的話)進行位置轉換
  4. 堆化:根據堆排序的邏輯對數組上的元素重新排序
    /**
     * 擴大數組容量
     * 添加元素時,如果當前數組已經無法添加新的元素時,那麼增加數組的大小。通常情況下是增加兩倍,但是如果達到最大值,最大值是 Integer.MAX_VALUE - 8,那麼拋出 OutOfMemoryError 異常。
     *
     * @param array 阻塞隊列的底層數組
     * @param oldCap 阻塞隊列的底層數組的長度
     */
    private void tryGrow(Object[] array, int oldCap) {
        // 在阻塞隊列擴容期間,先釋放鎖,以便其他線程能出隊和入隊操作。性能上的考慮。
        lock.unlock();
        Object[] newArray = null;
        // 由於已經釋放鎖,爲了保證擴容只有一個線程操作,所以使用 cas 修改 allocationSpinLock 控制
        if (allocationSpinLock == 0 &&
            UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                     0, 1)) {
            try {
                // 計算擴容後的大小
                int newCap = oldCap + ((oldCap < 64) ?
                                       (oldCap + 2) : // grow faster if small
                                       (oldCap >> 1));
                if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
                    int minCap = oldCap + 1;
                    // minCap<0 說明已經超過 Interger.MAX_VALUE,所以變成負值
                    // 如果超過上限則拋出 OutOfMemoryError 異常
                    if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                        throw new OutOfMemoryError();
                    newCap = MAX_ARRAY_SIZE;
                }
                // 創建一個新長度的數組作爲二叉堆的容器
                if (newCap > oldCap && queue == array)
                    newArray = new Object[newCap];
            } finally {
                allocationSpinLock = 0;
            }
        }
        // 如果 newArray == null 說明已經有其他的線程做擴容了,那麼將 CPU 的權限讓渡出去。
        if (newArray == null)
            Thread.yield();
        // 重新上鎖
        lock.lock();
        // 如果 newArray != null && queue == array 說明擴容成功,那麼將原來數組的數據拷貝到新創建的數組中,並將新數組作爲阻塞隊列的二叉樹容器
        // 如果是其他線程擴容,那麼將在調用該方法的 offer 方法中 while 判斷未擴容成功(n == cap),繼續執行 tryGrow 方法做擴容。
        if (newArray != null && queue == array) {
            queue = newArray;
            System.arraycopy(array, 0, newArray, 0, oldCap);
        }
    }
    
    /**
     * 二叉堆上浮
     *
     * @param k 添加元素的數組下標位置
     * @param x 入隊元素
     * @param array 存儲元素的數組
     */
    private static <T> void siftUpComparable(int k, T x, Object[] array) {
        // 獲取元素的比較策略用於比較大小
        Comparable<? super T> key = (Comparable<? super T>) x;
        // 遞歸做上浮操作,尋找二叉堆添加元素位置的過程。
        while (k > 0) {
            // 獲取父節點在數組上的位置
            int parent = (k - 1) >>> 1;
            // 獲取父節點的元素
            Object e = array[parent];
            // 比較大小。如果當前節點已經 大於或者等於父節點,那麼不需要繼續上浮操作,跳出循環。
            if (key.compareTo((T) e) >= 0)
                break;
            // 小於父節點,將父節點的元素賦值給比較的位置(每次遞歸操作比較位置會發生變化)
            array[k] = e;
            // 更改比較位置,調整成父節點。
            k = parent;
        }
        // 將元素賦值到定位到的二叉樹位置中
        array[k] = key;
    }
    
    /**
     * 二叉堆上浮。
     * 和上一個上浮操作邏輯唯一不同的地方是利用指定的比較策略比較。
     */
    private static <T> void siftUpUsingComparator(int k, T x, Object[] array,
                                       Comparator<? super T> cmp) {
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = array[parent];
            if (cmp.compare(x, (T) e) >= 0)
                break;
            array[k] = e;
            k = parent;
        }
        array[k] = x;
    }
    
    /**
     * 二叉堆下沉操作
     *
     * @param k 下沉開始的位置,一般是 0(二叉堆堆頂位置)
     * @param x 比較元素,一般是 0 指向的元素(二叉堆堆頂元素)
     * @param array 阻塞隊列數組
     * @param n 隊列長度
     */
    private static <T> void siftDownComparable(int k, T x, Object[] array,
                                               int n) {
        // 隊列中有元素才需要做下層操作,負責直接添加到 0 位置就可
        if (n > 0) {
            Comparable<? super T> key = (Comparable<? super T>)x;
            // 遞歸退出的臨界值。
            int half = n >>> 1;
            // 遞歸查找下沉位置
            while (k < half) {
                // 左子節點的位置
                int child = (k << 1) + 1;
                Object c = array[child];
                // 右子節點的位置
                int right = child + 1;
                // 比較左右節點的大小,如果左節點元素大於右節點元素,那麼將需要比較位置調成右子節點的位置,並將待比較的元素調整成右子節點指向的元素
                if (right < n &&
                    ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
                    c = array[child = right];
                // 如果待添加的元素已經小於或者等於左子節點,那麼跳出循環
                if (key.compareTo((T) c) <= 0)
                    break;
                // 將待比較的元素賦值給比較位置的下標,並將比較位置調整成葉子節點的位置,繼續遞歸查找合適的位置。
                array[k] = c;
                k = child;
            }
            // 已經確認合適的位置,將待添加的元素複製到該位置上
            array[k] = key;
        }
    }
    
    /**
     * 二叉堆下沉操作
     * 和上一個下沉操作唯一不同的地方在於使用指定的比較策略。
     */
    private static <T> void siftDownUsingComparator(int k, T x, Object[] array,
                                                    int n,
                                                    Comparator<? super T> cmp) {
        if (n > 0) {
            int half = n >>> 1;
            while (k < half) {
                int child = (k << 1) + 1;
                Object c = array[child];
                int right = child + 1;
                if (right < n && cmp.compare((T) c, (T) array[right]) > 0)
                    c = array[child = right];
                if (cmp.compare(x, (T) c) <= 0)
                    break;
                array[k] = c;
                k = child;
            }
            array[k] = x;
        }
    }
    
    /**
     * 堆化
     * 將阻塞隊列數組上的元素按照二叉堆排序規則重新排序
     */
    private void heapify() {
        Object[] array = queue;
        // 阻塞隊列長度
        int n = size;
        // 需要比較的次數。
        // 由於下沉操作本身比較會替換位置,所以不需要所有的元素都進行排序,只需要後半部分下沉或者前半部分上浮即可
        int half = (n >>> 1) - 1;
        Comparator<? super E> cmp = comparator;
        // 利用二叉堆下沉操作比較。
        // 下沉操作會比較左右子節點的大小並重新排序,比上浮操作會更加接近左葉子節點比右葉子節點元素小。
        if (cmp == null) {
            for (int i = half; i >= 0; i--)
                siftDownComparable(i, (E) array[i], array, n);
        }
        else {
            for (int i = half; i >= 0; i--)
                siftDownUsingComparator(i, (E) array[i], array, n, cmp);
        }
    }

迭代器

PriorityBlockingQueue 繼承自 Collection,所以也支持迭代器方法。但是由於二叉堆無法保證除了堆頂元素最小之外的其他元素的排序,所以遍歷元素時,除堆頂外的其他元素無法保證按照比較策略的順序出來。

    /**
     * 返回迭代器
     * PriorityBlockingQueue 利用內部類 Itr 實現迭代器相關功能。該方法實際上是返回 Itr 的一個實例。
     * 由於初始化迭代器時,利用阻塞隊列 toArray()創建了一個新數組,所以如果是在遍歷過程中,阻塞隊列發生變化,並不能正確的反映到迭代器上
     * @return an iterator over the elements in this queue
     */
    public Iterator<E> iterator() {
        return new Itr(toArray());
    }
    
    /**
     * 迭代器的實現類
     */
    final class Itr implements Iterator<E> {
        // 迭代器的數組。
        final Object[] array; // Array of all elements
        // 當前遍歷到的數組下邊
        int cursor;
        // 上一次遍歷到的數組下標
        int lastRet;
    
        Itr(Object[] array) {
            lastRet = -1;
            this.array = array;
        }
    
        /**
         * 如果 cursor 沒有到數組數組最後一個位置,返回 true,否則返回 false
         */
        public boolean hasNext() {
            return cursor < array.length;
        }
        /**
         * 返回下一個元素,並將遊標後移
         */
        public E next() {
            // 如果已經到達最後一個了元素了,該方法拋出 NoSuchElementException
            if (cursor >= array.length)
                throw new NoSuchElementException();
            lastRet = cursor;
            // 返回 cursor 遊標上的元素,並將 cursor 自增一
            return (E)array[cursor++];
        }
    
        /**
         * 刪除迭代器當前遍歷到的元素
         * 利用阻塞隊列 removeEQ 方法刪除阻塞隊列中的元素
         * 將 lastRet 置爲-1,表示當前遍歷的元素已經被刪除。
         */
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            removeEQ(array[lastRet]);
            lastRet = -1;
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章