[TOC]
PriorityBlockingQueue 1.8 源碼解析
一,簡介
PriorityBlockingQueue 是一個支持優先級的×××阻塞隊列,數據結構採用的是最小堆是通過一個數組實現的,隊列默認採用自然排序的升序排序,如果需要自定義排序,需要在構造隊列時指定Comparetor比較器,隊列也是使用ReentrantLock鎖來實現的同步機制。
二,UML圖
三,基本成員
// 數組的最大容量 2^31 - 8
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;
// 自旋鎖,在數組擴容時使用
private transient volatile int allocationSpinLock;
注意:這裏解釋下這個Integer.MAX_VALUE - 8,爲什麼數組的最大長度是這麼多了,這其實和int的最大值有關,最大值就是(1 << 32) -1 ,大家有沒有發現數組的長度類型是int,爲什麼是int了???我也不知道,我也試了其它數據類型發現數組的長度必須是int類型,哈哈,所以也可以理解爲什麼是最大值了,至於爲什麼要減八了,是因爲創建數組本身的信息(對象頭,class信息啊)也是需要存儲空間的,所以需要這8位的空間。
四,常用方法
入隊方法
add 方法
public boolean add(E e) {
return offer(e);
}
put 方法
由於是×××隊列所以put方法不會阻塞,也是直接調用了offer方法.
public void put(E e) {
offer(e); // never need to block
}
offer 帶超時方法
public boolean offer(E e, long timeout, TimeUnit unit) {
return offer(e); // never need to block
}
offer 方法
// 添加元素
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
int n, cap;
Object[] array;
// size大於等於數組的長度
while ((n = size) >= (cap = (array = queue).length))
// 擴容
tryGrow(array, cap);
try {
Comparator<? super E> cmp = comparator;
if (cmp == null) // 默認排序
siftUpComparable(n, e, array);
else // 自定義排序
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}
這裏我們主要分析下offer方法裏面的兩個重要方法,擴容和入隊,tryGrow,siftUpComparable方法。
tryGrow 方法
// 擴容方法
private void tryGrow(Object[] array, int oldCap) {
lock.unlock(); // must release and then re-acquire main lock
Object[] newArray = null;
// 只允許一個線程去擴容
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {
try {
// oldCap小於64 就加2 ,小於等於64就擴容50%
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // grow faster if small
(oldCap >> 1));
// 不可以超過MAX_ARRAY_SIZE
if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
int minCap = oldCap + 1;
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;
}
}
if (newArray == null) // back off if another thread is allocating
Thread.yield(); // 擴容獲取鎖失敗的線程,儘量讓出cpu
lock.lock(); // 重新獲取鎖
if (newArray != null && queue == array) {
queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
分析擴容:
- lock.unlock()第一行爲什麼就要釋放鎖了,因爲我們在入隊之前就獲取到了鎖,如果我們不釋放,那麼別的線程無法入隊也無法出隊,這就大大降低了併發性,釋放鎖,別的線程就可以進行入隊或者出隊操作。
- allocationSpinLock 這裏爲什麼要用這樣一個鎖了,其實和我們第一行的釋放鎖有關係,我們在擴容時釋放了鎖,那就代表了其它線程也可以入隊,但是隊列滿了,也需要擴容,所以這個鎖就是爲了讓擴容只有一個線程來操作。
- 在獲取了allocationSpinLock 鎖的線程在擴容中,我們發現其實只是創建了一個新的數組,並沒有數據的遷移啥的,這是爲什麼了???後面再解釋。
- newArray == null ,其實就是別的線程來爭奪擴容失敗,然後儘量讓出執行權(Thread.yield,線程從運行中變成就緒狀態),讓獲取鎖的線程去執行,但不是一定的,我們可以模擬一種可能,假設沒有讓出執行權,然後下一步獲取到了鎖。這時這個線程看見的newArray可能是null,所以就繼續走offer方法的while ((n = size) >= (cap = (array = queue).length))循環,直到擴容線程完成對newArray 的改變。
- lock.lock() 因爲我們在擴容前釋放了鎖,允許別的線程對數組的操作,所以獲取鎖的一方面目的是爲了控制只有一個線程對數組進行操作,第二個目的其實就是保證數組的可見性,別的線程可能在擴容期間執行了出隊操作,保證下面數組的拷貝是準確的數據。
siftUpComparable 方法
最小堆的構建
// 保證了每條鏈的順序小到大
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;
}
分析:
先得解釋下(k - 1) >>> 1,就是求的商,我們來模擬插入五個數吧,默認容量是11.
-
第一次插入一個1,此時的k是0,x是1,k不大於0,直接插入。
索引 0 值 1 -
第二次我們插入一個0,此時的k是1,x是0,parent是0,然後獲取0位置索引的值和現在的比較,現在其實是不大於0的,所以此時交換了位置,array[k] = e; k = parent;parent是0,所以結束循環然後在0的位置設置當前x是1。
索引 0 1 值 0 1 -
第三次我們插入一個5,此時的k是2,x是5,parent 是0,然後獲取0位置的值和插入值標記,發現是大於0的所以直接插入,在2的位置插入5。
索引 0 1 2 值 0 1 5 -
第四次我們插入一個4,此時的k是3,x是4,parent是1,然後獲取1位置的值和插入值比較,發現是大於0的,所以直接插入在3的位置插入。
索引 0 1 2 3 值 0 1 5 4 -
第五次我們插入一個3,此時的k是4,x是3,parent是1,然後獲取1位置值和插入值做比較,發現大於0的,所以直接在4的位置插入。
索引 0 1 2 3 4 值 0 1 5 4 3
我們用一個圖來描繪下這個數組,怎麼出現的這個圖了,我們發現每次插入的數的索引就是數組的長度,然後通過(i - 1)>>> 2 = n求父節點,通過比較和父節點比較確認自己的位置,左右子節點其實就是2n+1,2n+2,左右子節點就是數組的相鄰元素,我們發現子節點一定比父節點大,這就是最小堆;每次插入一個元素都是從最底層向上冒泡,維護最小堆的次序。
出隊方法
poll 方法
調用了 dequeue方法。
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 彈出根節點
return dequeue();
} finally {
lock.unlock();
}
}
// 帶超時時間
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
while ( (result = dequeue()) == null && nanos > 0)
nanos = notEmpty.awaitNanos(nanos);
} finally {
lock.unlock();
}
return result;
}
take 方法
也是調用了dequeue方法,這個方法支持線程的中斷。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
while ( (result = dequeue()) == null)
notEmpty.await();
} finally {
lock.unlock();
}
return result;
}
dequeue 方法
private E dequeue() {
int n = size - 1;
// 隊列還沒有初始化
if (n < 0)
return null;
else {
Object[] array = queue;
// 獲取根節點
E result = (E) array[0];
// 獲取尾節點
E x = (E) array[n];
// 尾節點置位null
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;
}
}
其實上面就是返回了根節點,然後獲取尾節點放在根節點的位置調整最小堆請看siftDownComparable方法。
siftDownComparable 方法
private static <T> void siftDownComparable(int k, T x, Object[] array,
int n) {
// n是數組的最大索引 k開始是0 x就是尾節點的值
if (n > 0) {
// x是最後一個節點的值
Comparable<? super T> key = (Comparable<? super T>)x;
int half = n >>> 1; // 最後一個節點的父節點 // loop while a non-leaf
while (k < half) { // k是頭節點 k> 了 說明到最後了
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]; // c就是右子節點
if (key.compareTo((T) c) <= 0) // 找到了子節點比自己大的
break;
array[k] = c;
k = child;
}
array[k] = key;
}
}
分析:
我們上圖的5個元素爲例,進行一次出隊操作。
- 彈出根節點0,然後調整最小堆。
-
我們調用siftDownComparable 方法調整最小堆,我們看下參數,此時的k是0,x是3,array就是這個數組,n就是4,key就是3,然後算half(half可以理解爲堆中父節點最大索引位置,找到這個節點說明已經沒有子節點了),half = 2。
- 第一次,從0開始,此時的child是1,c=1,right是2,c>arrray[right],不大於,此時的key小於c不小於,所以此時0的位置就變成了1(array[k] = c),k = child。
- 第二次,從1開始,此時的child是3,c是4,right是4,左邊大於右邊所以,c=array[child=right ] = 3,所以此時的child 是4,c是3,由於key <= c,所以借宿循環,直接在1的位置設置c,調整結束。
說下調整最小堆的過程,其實就是從根節點開始,重新構建父節點的過程,不過不是每個都需要重新構建,只需要構造子節點小的那邊的的父節點,因爲小的節點都去頂替原來的父節點了;我們彈出的是根節點,所以要從他的左右子節點找個根節點(但是要滿足子節點大於父節點的規則),那麼左右子節點有一個去當父節點了,它的位置也需要有節點代替,所以又從他的子節點開始找接替的節點,以此類推,直到找到最後一個父節點的位置。
size 方法
使用了鎖,這個是精確的值。
public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return size;
} finally {
lock.unlock();
}
}
五,總結
PriorityBlockingQueue 是一個wujie的隊列,使用put方法不會阻塞,使用時一定要注意內存溢出的問題;整個隊列的出隊和入隊都是通過最小堆來實現的,理解最小堆是這個隊列的關鍵;這個一個優先級的隊列,適合有優先級的場景。
參考《Java 併發編程的藝術》