概述
PriorityBlockingQueue:二叉堆結構優先級阻塞隊列,FIFO,通過顯式的lock鎖保證線程安全,是一個線程安全的BlockingQueue,加入隊列的數據實例按照指定優先級升序排列,這個規則通過賦值 實現了Comparator的字段或數據實例類實現Comparable接口自定義,都定義的情況下 字段比較器優先。它一個老牌的隊列,在JDK1.5已經加入,如果隊列加入的數據實例需要排序,這是個不錯的選擇。
核心屬性和數據結構
private static final int DEFAULT_INITIAL_CAPACITY = 11;//默認數組容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;//數組最大容量
private transient Object[] queue;//存儲數據的數組
private transient int size;//節點數 統計用
private transient Comparator<? super E> comparator;//比較器,可自定義傳入
private final ReentrantLock lock;//重入鎖,依賴它實現線程安全
private final Condition notEmpty;//lock創建的 隊列不爲空控制條件。當隊列爲空阻塞,當新數據實例加入喚醒。
private transient volatile int allocationSpinLock;//初始值爲0爲可分配,用於控制擴容
private PriorityQueue q;//內部PriorityQueue引用,用於兼容序列化
二叉堆
PriorityBlockingQueue:內部數據由Object數組存儲,這個數組本質上是一個二叉堆。
二叉堆是特殊的二叉樹,它等於或接近完全二叉樹,二叉堆滿足特性:二叉堆的父節點總是子節點保持有序關係,並且每個節點的子節點也是二叉堆
當父節點的鍵值總是大於或等於任何一個子節點的鍵值,叫做最大堆,當父節點的鍵值總是小於或等於任何一個子節點的鍵值,叫做最小堆。
二叉堆總是用數組表示,如果根節點的位置是1,那麼n節點的子節點分別是2n和2n+1,如果節點1的子節點在 2,3。 節點2的子節點在4,5 以此類推,根節點等於1,方便了子節點的推算,如果存儲的數組下標從0開始。那麼節點N的子節點分別是2n+1 和2(n+1),其父節點是(n-1)/2,PriorityBlockingQueue中使用的就是基於下標0的二叉堆。
源碼分析
加入數據實體
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
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;
if (cmp == null)//字段比較器 優先
siftUpComparable(n, e, array);//這裏siftUp 理解爲新加入的節點,經過比較儘量向數組後面移動
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;//統計節點數
notEmpty.signal();//喚醒阻塞
} finally {
lock.unlock();
}
return true;
}
siftUpComparable和siftUpUsingComparator實現邏輯相似,區別在於使用了不同的比較器。通常情況下使用節點比較器, 節點數據類實現Comparable接口。
private static <T> void siftUpComparable(int k, T x, Object[] array) {
Comparable<? super T> key = (Comparable<? super T>) x;
//到這 k是要寫入的索引,key是要寫入的值
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;
}
邏輯說明:
PriorityBlockingQueue是最小二叉堆,新插入的數據是子節點,找到父節點比較,大於等於父節點,跳出循環,小於父節點,交換位,key==0跳出循環,key小於0不可能出現。交換的過程是將父節點數據插入k位置,然後將父節點索引賦值給k。 等找到k的最終位置,新插入的數據插入 位置k。
假設插入Task 實例,按照 id=5,4,3,2,1的順序添加,按照id大小排序。 剛添加後的數組數據id排列如 :1,2,4,5,3
就是說插入後的數據不是嚴格遞增的,得到這個結果我也很驚訝,而經過測試 循環take取出的數據是嚴格遞增的,能推測出take也做了排序處理,下面詳細分析。
獲取數據
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();//可打斷鎖
E result;
try {
while ( (result = extract()) == null)//如果抽取的數據等於null,阻塞直到被喚醒。
notEmpty.await();
} finally {
lock.unlock();
}
return result;
}
private E extract() {
E result;
int n = size - 1;//獲取數組有效數據最大索引值
if (n < 0)
result = null;
else {
Object[] array = queue;
result = (E) array[0];//獲取0位置數據
E x = (E) array[n];//獲取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;
}
//取出0位置的索引值後,從上到下調整父節點和子節點,達到排序的目的
private static <T> void siftDownComparable(int k, T x, Object[] array,
int n) {
//注意 k==0,x是array中最後面的有效數據,n是有效數據的最大索引值。
Comparable<? super T> key = (Comparable<? super T>)x;
int half = n >>> 1; // loop while a non-leaf
while (k < half) {//k如果等於half 那麼k的子節點將是n位置的數據,不管順序問題,整體的數據節點在數組中是要前移一位的,n位置不存在
//將二叉堆最後位置n的數據放入0位置,從上到下調整父節點和子節點位置
int child = (k << 1) + 1; // assume left child is least
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;
}
本質上是刪除根節點,將二叉堆最後位置的節點放入根節點,然後從上到下調整父節點和子節點的位置。
爲什麼這樣搞能保證有序?
下面來分析這個問題
假設id 1~9 按照逆序加入PriorityBlockingQueue,隊列中順序 是[1-id爲1, 2-id爲2, 4-id爲4, 3-id爲3, 7-id爲7, 8-id爲8, 5-id爲5, 9-id爲9, 6-id爲6],組成的二叉堆如下圖
由於PriorityBlockingQueue中的數組是最小二叉堆,葉子節點大於等於父節點,所以層級低(1層級高,9和6層級低)的節點與根節點差值越大
多次take二叉堆變化如下圖
總結一下變化 每次取出根節點,放入尾部節點值,都將其所有父節點向上推動一層,如果沒有父節點,將兄弟節點向上推動一層。由於 根節點與下一層的差值是最小的 並且上升後的根目錄比子節點都小,所以每次取出的根節點都距離上次取出的根目錄差值最小,所以取出的節點是嚴格有序的。
總結:本文重點理解二叉堆數據結構,和PriorityBlockingQueue藉助二叉堆怎麼實現排序的。