源碼閱讀(37):Java中線程安全的Queue、Deque結構——PriorityBlockingQueue

1、概述

PriorityBlockingQueue是一種無界阻塞隊列,其內部核心結構和我們前文中已經介紹過的PriorityQueue隊列集合類似,都是基於小頂堆樹進行工作。本文不會贅述介紹PriorityQueue時已經詳解過的內容(《源碼閱讀(12):Java中主要的Queue、Deque結構——PriorityQueue集合》),例如小頂堆樹的工作原理等。本文將集中精力在幾個PriorityBlockingQueue隊列集合的核心方法的介紹上,這些方法都是確保PriorityBlockingQueue隊列在多線程場景下能正確工作的重要方法。

PriorityBlockingQueue隊列本身的使用屬於最基本的知識,並不是本文介紹的重點,和之前的規矩類似,給出一個簡單的使用實例即可:

// ......
// 創建一個PriorityBlockingQueue對象
PriorityBlockingQueue<Integer> queue = new PriorityBlockingQueue<>(16 , new Comparator<Integer>() {
  @Override
  public int compare(Integer o1, Integer o2) {
    return o1 - o2;
  }
});
// 向priorityQueue集合添加數據
queue.add(11);
queue.add(88);
queue.add(8);
queue.add(19);
queue.add(129);
// ......
queue.add(15);
queue.add(198);
queue.add(189);
queue.add(200);
// 從PriorityBlockingQueue集合移除數據
for (int index = 0 ; index < queue.size() ; ) {
  System.out.println("priorityBlockingQueue item = " + queue.poll());
}
// ......

注意,由於小頂堆的工作特點,所以PriorityBlockingQueue隊列集合PriorityQueue隊列集合一樣,也只是保證數組頭部將要去取的數據對象滿足權值最小的要求。

2、PriorityBlockingQueue核心結構

2.1、PriorityBlockingQueue主要屬性

爲了瞭解PriorityBlockingQueue隊列集合如何支持在多線程環境下的正常工作,我們需要首先分析一下這個隊列集合具有哪些重要屬性,如下所示:

// ......
public class PriorityBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
  // ......
  // 該常量用來描述該隊列默認的初始化容量
  private static final int DEFAULT_INITIAL_CAPACITY = 11;
  
  // 實際上PriorityBlockingQueue隊列本質上還是有界的(只是這個界限非常大),
  // 該常量用來描述該隊列支持的最大容量上限
  private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
  /**
   * 就像上文中介紹PriorityQueue時提到的,其內部基於小頂堆樹工作(小頂堆樹是一種平衡二叉樹),
   * 而小頂堆樹在java的集合框架中,往往有以數組形式進行表單(一種樹結構的降維表達)。
   * 所以PriorityBlockingQueue內部也是使用數組進行數據對象存儲。樹節點的左右兒子在數組中的索引定位具有以下特點:
   * 如果記當前節點在索引中的存儲位置爲n,其左兒子的索引位爲2*n+1,其右兒子的索引位爲2*(n+1)。
   */
  private transient Object[] queue;
  /**
   * 該值記錄當前PriorityBlockingQueue隊列集合的大小
   * 請注意容量和大小的區別。
   */
  private transient int size;
  /**
   * 用於當前隊列集合中數據對象排序的比較器,如果該比較器爲null
   * 那麼將使用數據對象自帶的比較器進行排序比較
   */
  private transient Comparator<? super E> comparator;
  // 通過這個鎖對象,控制該隊列集合所有公共寫方法的線程安全性
  private final ReentrantLock lock = new ReentrantLock();
  // 該控制條件對象基於以上的可重入鎖,方便在隊列至少有一個數據對象時,喚醒可能處於阻塞狀態的消費者線程
  // 或者在隊列中沒有數據對象時,讓消費者線程進入阻塞狀態
  @SuppressWarnings("serial") // Classes implementing Condition may be serializable.
  private final Condition notEmpty = lock.newCondition();
  // SpinLock 代表自旋鎖,這個變量用於隊列集合的擴容過程
  // 它保證擴容過程不會重複進行,且儘可能少的產生性能影響(不好想象?後文將會詳細介紹)
  private transient volatile int allocationSpinLock;
  // 該屬性僅用於PriorityBlockingQueue對象的序列化和反序列化過程
  // 這是一個巧妙的設計,避免多個JDK版本間進行對象序列化和反序列的過程中,發生兼容性問題
  private PriorityQueue<E> q;
  // ......
} 
// ......

PriorityBlockingQueue通過ReentrantLock可重入鎖保證多線程場景下隊列集合的安全性,這個思路和之前我們講解過的ArrayBlockingQueue、LinkedBlockingQueue等隊列集合保證線程安全的性的思路大同小異。但是allocationSpinLock和q這兩個屬性幕後的設計思路值得我們在後文進行詳細介紹。

2.2、PriorityBlockingQueue主要構造函數

PriorityBlockingQueue隊列集合一共有4個構造函數,其中三個都很簡單和PriorityQueue中的構造函數類似,如下所示:

// ......
public class PriorityBlockingQueue<E> extends AbstractQueue<E>
    implements BlockingQueue<E>, java.io.Serializable {
  // ......
  // 默認的構造函數,這是隊列沒有設置公共的比較器
  // 且隊列集合初始化容量爲11(DEFAULT_INITIAL_CAPACITY常量決定)
  public PriorityBlockingQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);
  }

  // 使用該構造函數初始化對象時,
  // 隊列沒有設置公共的比較器,但是可以傳入一個初始化的容量大小
  // 該初始化容量大小應該大於1,否則會拋出異常
  public PriorityBlockingQueue(int initialCapacity) {
    this(initialCapacity, null);
  }

  // 以上兩個構造函數,實際上都是對這個構造函數的調用
  // 可以設定公共的比較器,以及設定隊列集合的初始化容量大小
  public PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator) {
    if (initialCapacity < 1) {
      throw new IllegalArgumentException();
    }
    this.comparator = comparator;
    this.queue = new Object[Math.max(1, initialCapacity)];
  }
  // ......
} 
// ......

以上三個構造函數都很簡單,第四個構造函數:PriorityBlockingQueue(Collection<? extends E> c),則是參考一個外部集合完成PriorityBlockingQueue對象的實例化,這個被參考的集合不能爲null:

// ......
public PriorityBlockingQueue(Collection<? extends E> c) {
  // 如果該變量爲true,說明經過處理邏輯,並不知道當前集合隊列中的數據對象是否是有序的
  // true if not known to be in heap order
  boolean heapify = true; 
  // 如果該變量爲true,說明經過處理邏輯,並不能排除當前集合隊列中的數據對象沒有爲null的情況
  // 所以需要進行排查
  // true if must screen for nulls
  boolean screen = true;  
  // 如果條件成立,說明源集合的數據對象是有序排列的
  // 並且嘗試獲取SortedSet集合中可能存在的排序器
  if (c instanceof SortedSet<?>) {
    SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
    this.comparator = (Comparator<? super E>) ss.comparator();
    heapify = false;
  }
  // 如果條件成立,說明源集合本來就是一個PriorityBlockingQueue隊列集合
  // 那麼源集合中可能存在的排序器,就是新的PriorityBlockingQueue隊列集合的排序器
  else if (c instanceof PriorityBlockingQueue<?>) {
    PriorityBlockingQueue<? extends E> pq = (PriorityBlockingQueue<? extends E>) c;
    this.comparator = (Comparator<? super E>) pq.comparator();
    // 不需要進行數據對象爲null的排除操作
    screen = false;
    // 這個判定條件成立,說明當前的源集合c完全匹配PriorityBlockingQueue隊列集合
    // 這是多餘的判定嗎?顯然不是,例如源集合c如果是PriorityBlockingQueue的子類對象,這個判定結果就爲false
    // exact match
    if (pq.getClass() == PriorityBlockingQueue.class) {
      heapify = false;
    }
  }
  
  // c不能爲null,否則這裏會報錯
  // es記錄了源集合c中的數據對象數組。
  Object[] es = c.toArray();
  int n = es.length;
  // 如果es並不是一個一維數組,那麼通過這裏的操作轉換爲一維數組
  // If c.toArray incorrectly doesn't return Object[], copy it.
  if (es.getClass() != Object[].class) {
    es = Arrays.copyOf(es, n, Object[].class);
  }

  // ==========接下來開始進行數據對象的清理工作
  if (screen && (n == 1 || this.comparator != null)) {
    for (Object e : es) {
      if (e == null) {
        throw new NullPointerException();
      }
    }
  }
  this.queue = ensureNonEmpty(es);
  this.size = n;
  // 如果裝入當前PriorityBlockingQueue隊列集合的數據對象數組需要被重新排列
  // 則通過該方法進行小頂堆排序
  if (heapify) {
    heapify();
  }
}
// ......

2.3、PriorityBlockingQueue擴容過程

在講解PriorityQueue隊列對,我們就已經介紹過擴容過程,PriorityBlockingQueue隊列集合和PriorityQueue隊列集合的擴容過程都是類似的。不過由於PriorityBlockingQueue被設計工作在多線程場景下,所以其擴容操作針對這樣的工作場景做了有針對性的優化,我們先來看一下PriorityBlockingQueue隊列集合中關於擴容操作部分的代碼:

// ......
/**
 * Tries to grow array to accommodate at least one more element
 * (but normally expand by about 50%), giving up (allowing retry)
 * on contention (which we expect to be rare). Call only while
 * holding lock.
 * 注意:官方註釋中的允許重試,並不在該方法本身的處理邏輯內,
 * 而是在調用者的while()循環中
 */
private void tryGrow(Object[] array, int oldCap) {
  // tryGrow方法主要由offer(E)方法進行調用
  // 調用tryGrow方法的第一個操作,就是釋放當前線程獲取的鎖操作權
  // 改用CAS思想進行擴容操作
  // must release and then re-acquire main lock
  lock.unlock();
  // 該變量將決定當前線程是否進行了實際的擴容操作
  Object[] newArray = null;
  // 從JDK9+開始,該判斷條件變成了現有的語句,實際上和之前版本中使用UNSAFE.compareAndSwapInt()方法的目的一致:
  // 原子性的變更allocationSpinLock屬性的值爲1,保證成功設置allocationSpinLock爲1的線程
  // 能進行真正的擴容操作
  if (allocationSpinLock == 0 && ALLOCATIONSPINLOCK.compareAndSet(this, 0, 1)) {
    try {
      // 實際的擴容邏輯和PriorityQueue集合隊列的擴容邏輯一致:
      // 如果原始容量小於64,那麼就進行雙倍擴容(實際上是雙倍容量+2)
      // 如果原始容量大於64,那麼進行50%的擴容。
      int newCap = oldCap + ((oldCap < 64) ?
                              (oldCap + 2) : // grow faster if small
                              (oldCap >> 1));
      // 如果條件成立,說明擴容後,新的容量已經超過了最大允許的容量上限
      // 那麼就以最大允許的容量作爲新的容量
      if (newCap - MAX_ARRAY_SIZE > 0) {
        int minCap = oldCap + 1;
        if (minCap < 0 || minCap > MAX_ARRAY_SIZE) {
          throw new OutOfMemoryError();
        }
        newCap = MAX_ARRAY_SIZE;
      }
      // 基於擴容後的新的容量,初始化一個數組
      // 這個數組將在隨後的操作中替換掉當前隊列集合正在使用的queue數組
      if (newCap > oldCap && queue == array) {
        newArray = new Object[newCap];
      }
    } finally {
      // 操作完成後,變更allocationSpinLock屬性爲0
      // 以便進行下一次擴容操作。
      allocationSpinLock = 0;
    } 
  }
  // 如果條件成立,說明當前操作線程沒有獲取到進行擴容的實際操作權
  // 這時讓當前線程讓出CPU資源(傳統意義上講的降低優先級)
  // 這樣保證完成實際擴容操作的線程,能夠隨後搶佔到鎖權限
  // back off if another thread is allocating
  if (newArray == null) {
    Thread.yield();
  }
  lock.lock();
  // 進行實際的擴容操作——進行數組拷貝
  if (newArray != null && queue == array) {
    queue = newArray;
    System.arraycopy(array, 0, newArray, 0, oldCap);
  }
}
// ......

擴容操作的發生條件是一種極端場景——集合內用於存儲數據對象的queue數組不夠用了,且都是由隊列的生產者線程發起擴容操作。這種場景下擴容操作者通過整個對象共享的可重入鎖獲取了操作權限,但實際上擴容操作只對數據添加操作有影響,對PriorityBlockingQueue隊列集合的數據讀取並沒有影響。

爲了使得擴容操作的同時,其它消費者線程能繼續從隊列中取出數據對象,所以擴容操作釋放了可重入鎖的佔用狀態。但這又引來一個新問題,既是可能有多個生產者線程同時調用擴容請求,而擴容請求又不能重複操作,否則會造成queue數組大小(隊列容量上限)數值錯誤。

爲了避免出現的新問題,PriorityBlockingQueue隊列集合改用CAS原理控制多個生產者線程同時調用擴容操作——保證同一時間只有一個擴容請求得到實際操作,其餘擴容操作保持自旋,直到擴容操作結束

2.4、PriorityBlockingQueue序列化和反序列過程

PriorityBlockingQueue隊列集合的序列化和反序列化過程也是一對很有趣的操作過程,從JDK 1.5+的版本到JDK 14的版本,雖然PriorityBlockingQueue隊列集合的基本屬性和和核心處理思想都沒有太大變化,但是爲了保證序列化後的信息能夠被多個版本很好的兼容讀取(反序列化),PriorityBlockingQueue隊列集合中使用了一個取代的PriorityQueue對象,來實現這樣的兼容性目標。

  • 序列化過程
// ......
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
  // 序列化操作也需要獲得操作權限
  lock.lock();
  try {
    // 初始化一個PriorityQueue隊列集合,幫助完成序列化過程
    q = new PriorityQueue<E>(Math.max(size, 1), comparator);
    // 將當前集合中的數據對象添加到這個PriorityQueue隊列集合中
    q.addAll(this);
    // 然後將q屬性引用的PriorityQueue隊列集合進行序列化
    // 這樣就達到了使用PriorityQueue保證兼容性
    s.defaultWriteObject();
  } finally {
    // 序列化完成後 q 屬性就沒有用了,設爲null
    // 最後釋放操作權限
    q = null;
    lock.unlock();
  }
}
// ......
  • 反序列化過程
// ......
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
  try {
    // 使用該方法從序列化信息中還原PriorityBlockingQueue隊列集合中的基本屬性
    // 其中就包括了q屬性代表的PriorityQueue對象
    s.defaultReadObject();
    int sz = q.size();
    SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, sz);
    // 爲新的PriorityBlockingQueue對象初始化queue數組
    this.queue = new Object[Math.max(1, sz)];
    // 爲新的PriorityBlockingQueue對象初始化可能的比較器
    comparator = q.comparator();
    // 將PriorityQueue隊列集合中的數據對象賦值到新的PriorityBlockingQueue隊列集合中
    addAll(q);
  } finally {
    // 反序列化過程結束後,q屬性就完成了它的工作目標
    // 設置爲null
    q = null;
  }
}
// ......

2.5、PriorityBlockingQueue的典型操作方法

PriorityBlockingQueue中的多種典型操作方法,其核心邏輯思路已經在介紹PriorityQueue隊列集合時進行了詳細說明,這裏不再進行贅述。

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