PriorityBlockingQueue
需要對堆排序有了解,推薦 排序六 堆排序
PriorityBlockingQueue
底層是個最小堆。當存儲元素的數組滿時插入操作不會阻塞,而是擴容數組;數組空時 take 方法阻塞。
// 默認數組大小,該數組是可擴容的
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;
// 擴容時用到的鎖,使用 CAS + volatile 實現
private transient volatile int allocationSpinLock;
插入操作
offer
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);
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
notEmpty.signal(); // “喚醒”取線程
} finally {
lock.unlock();
}
return true;
}
siftUpComparable
上浮操作,可以看出這是個最小堆。就是不斷與其父進行比較,直到找到合適位置,注意數組從0開始,與從1開始的堆算法在找尋父元素是不同的。
private static <T> void siftUpComparable(int k, T x, Object[] array) {
Comparable<? super T> key = (Comparable<? super T>) x;
// 不斷往上找尋合適位置插入
while (k > 0) {
// 父節點位置,這裏的堆是從0開始的,
// 若是你自己寫堆算法,先確定是從0還是從1開始,
// 二者找尋父/子節點的算法不同
int parent = (k - 1) >>> 1;
Object e = array[parent];
if (key.compareTo((T) e) >= 0) // 這是最小堆
break;
array[k] = e;
k = parent;
}
array[k] = key;
}
tryGrow
關於擴容操作有哪些需要注意的?
需要一直持有獨佔鎖嗎?PriorityBlockingQueue 採用的是獨佔鎖,擴容一直持有鎖會阻塞所有操作,而即使數組滿了也不應阻止 take,peek 等取操作,所以不應一直持有鎖,但將舊數組元素拷貝到新數組以及賦值操作應該受到該獨佔鎖的保護,所以擴容前應釋放鎖,然後計算大小並創建新數組,之後獲取鎖執行賦值拷貝操作。
計算大小並創建新數組的過程不需要鎖保護?併發下若多個線程都發起擴容操作,而該過程不加鎖,它們皆得到一個新線程,之後輪流獲取鎖對舊數組進行拷貝及賦值,所以該過程是需要鎖的,而且是排它鎖。我們可以利用 CAS + volatile 來實現一個排他的鎖。
下面代碼便是對上述的實現:
private void tryGrow(Object[] array, int oldCap) {
lock.unlock(); // 釋放鎖
Object[] newArray = null;
// allocationSpinLock 是volatile的,獲取鎖就是CAS將其從0改爲1
// 有且只有一個線程會成功,從而保證只有一個線程計算並創建新數組
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;
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,讓渡執行權限
if (newArray == null) // back off if another thread is allocating
Thread.yield();
lock.lock(); // 獲取鎖
// 賦值及拷貝數據
if (newArray != null && queue == array) {
queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
還有一個問題:allocationSpinLock = 0; 釋放鎖後,此時又一線程CAS成功進行創建新數組操作,由於獨佔鎖用的是 ReentrantLock ,且用到是非公平模式,那麼這兩個究竟誰會先獲取鎖執行接下來的賦值拷貝操作是說不準的,而且併發下可能並不止兩個,其實誰先並不重要,重要的是隻能有一個執行賦值拷貝操作,也就是這一情況下最先獲取鎖的線程在執行賦值拷貝操作後,其它的線程在獲取鎖後並不會執行賦值拷貝操作。說了這麼多解決辦法是什麼?就是 if 判斷中的 queue == array
,有了這句就能保證上述情況下的安全。那麼 if 中第一個判斷條件newArray != null
處理的是什麼情況?它處理的是那些CAS失敗的線程,它們不會執行創建新數組操作,也就是 newArray 爲 null,它們會讓渡執行機會,之後獲取鎖後我們也不希望它們執行賦值拷貝操作。
add,put
都是調用 offer,注意 put 不阻塞,不同於 ArrayBlockingQueue 與 LinkedBlockingQueue。
public void put(E e) {
offer(e); // never need to block
}
public boolean add(E e) {
return offer(e);
}
獲取操作
peek
最小堆,堆頂即爲優先級最大的元素。
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (size == 0) ? null : (E) queue[0];
} finally {
lock.unlock();
}
}
刪除操作
remove
public boolean remove(Object o) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
int i = indexOf(o); //遍歷數組找尋對象
if (i == -1)
return false;
removeAt(i);
return true;
} finally {
lock.unlock();
}
}
堆排序的刪除操作:將最後一個元素取出並刪除,與刪除位置元素比較,若該刪除位置有子節點,則先執行下沉操作,下沉即往下找到合適位置的操作,若無子節點或是比刪除位置節點小,那麼執行上浮操作,也就是往上找合適位置。
private void removeAt(int i) {
Object[] array = queue;
int n = size - 1;
if (n == i) // 若是末尾則直接刪除
array[i] = null;
else {
E moved = (E) array[n]; // 獲取末尾元素
array[n] = null; // 刪除末尾位置
Comparator<? super E> cmp = comparator;
if (cmp == null)
// 下沉操作,往下找到moved 的合適位置
siftDownComparable(i, moved, array, n);
else
siftDownUsingComparator(i, moved, array, n, cmp);
// 刪除位置無子節點或moved比其小,上浮,往上找合適位置
if (array[i] == moved) {
if (cmp == null)
siftUpComparable(i, moved, array);
else
siftUpUsingComparator(i, moved, array, cmp);
}
}
size = n;
}
上浮操作已分析過,下面來分析下沉操作:
首先得知道刪除位置有無子節點,無子節點也就會用往下尋找;有子節點,因爲是最小堆,將元素與較小的子節點比較,若是小於等於它則當前位置即合適位置,否則沿着較小的子節點往下繼續尋找。
private static <T> void siftDownComparable(int k, T x, Object[] array,
int n) {
if (n > 0) {
Comparable<? super T> key = (Comparable<? super T>)x;
int half = n >>> 1; // >= half 位置的節點都沒子節點
while (k < half) {
int child = (k << 1) + 1; // 獲取 k 的左子節點位置
Object c = array[child];
int right = child + 1; // 右子節點位置
// 比較得到較小的那個賦給 c
if (right < n &&
((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
c = array[child = right];
// 若小於等於 c ,則 k 就是合適的插入位置
if (key.compareTo((T) c) <= 0)
break;
array[k] = c;
k = child;
}
array[k] = key;
}
}
poll
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return dequeue();
} finally {
lock.unlock();
}
}
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];
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;
}
}
彈出的是堆頂元素,將末尾元素從頂開始往下找尋合適位置插入。
take
數組爲空時阻塞。
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;
}