JUC源碼分析-容器-PriorityBlockingQueue

概述

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藉助二叉堆怎麼實現排序的。

 

 

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