【Java並發編程】18、PriorityBlockingQueue源碼分析,
PriorityBlockingQueue是一個基於數組實現的安全執行緒的無界隊列,原理和內部結構跟PriorityQueue基本一樣,只是多了個安全執行緒。javadoc裡面提到一句,1:理論上是無界的,所以添加元素可能導致outofmemoryerror;2.不容許添加null;3.添加的元素使用構造時候傳入Comparator排序,要不然就使用元素的自然排序。
PriorityBlockingQueue是基於優先順序,不是FIFO,這是個好東西,可以用來實現優先順序的線程池,高優先順序的先執行,低優先順序的後執行。跟之前看過的幾個隊列一樣,都是繼承AbstractQueue實現BlockingQueue介面。
對於優先順序的實現,是採用數組來實現堆的,大概樣子畫個圖容易理解:
堆頂元素時最小的,對於各左右子堆也保證堆頂元素最小。
內部結構和構造:
//基於數組實現的,如果構造沒有傳入容量,就是用預設大小private static final int DEFAULT_INITIAL_CAPACITY = 11;/** * 數組最大容量 */private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;/** * 優先順序隊列數組,記住queue[n]的2個左右子項目在數組的位置為在queue[2*n+1]和queue[2*(n+1)] */private transient Object[] queue;/** * 隊列元素個數 */private transient int size;/** * 比較子,構造時可以選擇傳入,沒有就null,到時候就使用元素的自然排序 */private transient Comparator<? super E> comparator;/** * 重入鎖控制多有操作 */private final ReentrantLock lock;/** * 隊列為空白的時候條件隊列 */private final Condition notEmpty;/** * 自旋鎖 */private transient volatile int allocationSpinLock;/** * 序列化的時候使用PriorityQueue,這個PriorityBlockingQueue幾乎一模一樣 */private PriorityQueue q;/** * 預設構造,使用預設容量,沒有比較子 */public PriorityBlockingQueue() { this(DEFAULT_INITIAL_CAPACITY, null);}public PriorityBlockingQueue(int initialCapacity) { this(initialCapacity, null);}/** * 最終調用的構造 */public PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator) { if (initialCapacity < 1) throw new IllegalArgumentException(); this.lock = new ReentrantLock(); this.notEmpty = lock.newCondition(); this.comparator = comparator; this.queue = new Object[initialCapacity];}
內部結構和構造沒有什麼特別的地方,基於數組實現優先順序的堆,記住數組元素queue[n]的左節點queue[2*n+1]和右節點queue[2*(n+1)],每次出隊的都是queue[0]。
看下常用方法:
add、put、offer都是最終調用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(); //添加後通知非空條件隊列可以take } finally { lock.unlock(); } return true;}//數組擴容private void tryGrow(Object[] array, int oldCap) { lock.unlock(); // 數組擴容的時候使用自旋鎖,不需要鎖主鎖,先釋放 Object[] newArray = null; if (allocationSpinLock == 0 && UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset, 0, 1)) { //cas佔用自旋鎖 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; //擴容後釋放自旋鎖 } } if (newArray == null) // 到這裡如果是本線程擴容newArray肯定是不為null,為null就是其他線程在處理擴容,那就讓給別的線程處理 Thread.yield(); lock.lock(); //這裡重新重入鎖,因為擴容後還有其他動作 if (newArray != null && queue == array) { //這裡不為null那就複製數組 queue = newArray; System.arraycopy(array, 0, newArray, 0, oldCap); }}//所有插入都用從下向上調整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) //如果比父節點大,那就無所謂退出,直接放在k位置 break; array[k] = e; //比父節點小,按照k位置給父節點,然後從父節點開始繼續向上尋找 k = parent; } array[k] = key;}//所有插入都用從底向上調整,跟siftUpComparable方法類似就是比較的時候使用了構造傳入的comparatorprivate static <T> void siftUpUsingComparator(int k, T x, Object[] array, Comparator<? super T> cmp) { while (k > 0) { int parent = (k - 1) >>> 1; Object e = array[parent]; if (cmp.compare(x, (T) e) >= 0) break; array[k] = e; k = parent; } array[k] = x;}
所有的添加元素最後都是調用offer方法,2步:擴容+儲存,大體流程為:
1.加鎖,檢查元素數量是否大於等於數組長度,如果是,那就擴容,擴容沒必要使用主鎖,先釋放鎖,使用cas自旋鎖,容量最少翻倍,釋放自旋鎖,可能存在競爭,檢查下,是否擴容,如果擴容那就複製數組,再度加主鎖;
2.看構造入參是否有comparator,有就使用,沒有就自然排序,從數組待插入位置父節點開始比較大,如果大於父節點,那就直接待插入位置插入,否則就跟父節點交換,然後迴圈向上尋找,數量加1,通知非空條件隊列take,最後釋放鎖。
看下幾個出隊操作:
public E poll() { final ReentrantLock lock = this.lock; lock.lock(); try { return dequeue(); } finally { lock.unlock(); }}public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); //響應中斷 E result; try { while ( (result = dequeue()) == null) notEmpty.await(); //如果take,數組沒有元素是要阻塞的 } finally { lock.unlock(); } return result;}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;}public E peek() { final ReentrantLock lock = this.lock; lock.lock(); try { return (size == 0) ? null : (E) queue[0]; //只是擷取元素,不移除 } 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; }}//從頂向下調整private static <T> void siftDownComparable(int k, T x, Object[] array, int n) { if (n > 0) { //元素數量大於0,數組非空 Comparable<? super T> key = (Comparable<? super T>)x; int half = n >>> 1; // 最後一個葉子節點的父節點位置 while (k < half) { int child = (k << 1) + 1; // 待調整位置左節點位置 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) //如果待調整key最小,那就退出,直接賦值 break; array[k] = c; //如果key不是最小,那就取左右節點小的那個放到調整位置,然後小的那個節點位置開始再繼續調整 k = child; } array[k] = key; }}
出隊的大體流程:
1.加鎖,擷取queue[0],清掉堆的最後一個葉子節點,並將其作為比較節點。等價於把最後一個葉子節點移到了queue[0]位置。然後從頂向下比較,找到新的queue[0]應該在的位置
2.調用從頂向下調整的方法:待調整位置節點左右節點和之前的葉子節點比較,如果之前葉子節點最小,那就直接放入待調整位置,如果是葉子節點小,那就取小的那個放入待調整位置,並且將小的部分重新迴圈尋找,迴圈次數根據2分尋找,基本是元素數量的一半就到找到位置。
再看一個remove,因為remove方法,2中調整方式都用到了:
public boolean remove(Object o) { final ReentrantLock lock = this.lock; lock.lock(); try { int i = indexOf(o); //尋找o在數組中位置 if (i == -1) return false; removeAt(i); //remove掉 return true; } finally { lock.unlock(); }}//o在數組中的位置private int indexOf(Object o) { if (o != null) { Object[] array = queue; int n = size; for (int i = 0; i < n; i++) if (o.equals(array[i])) return i; } return -1;}//remove掉數組指定位置的元素//跟之前take的dequeue相似的地方,dequeue是remove掉0的位置,然後調整也是從0的位置開始調整,這裡是從指定位置調整private void removeAt(int i) { Object[] array = queue; int n = size - 1; if (n == i) // removed last element array[i] = null; else { E moved = (E) array[n]; //跟dequeue一樣也是最後一個葉子節點作為比較 array[n] = null; Comparator<? super E> cmp = comparator; if (cmp == null) siftDownComparable(i, moved, array, n); //從指定位置調整 else siftDownUsingComparator(i, moved, array, n, cmp); //經過從上向下調整後,如果是直接將比較節點放在待調整位置,那隻能說明這個節點在以它為堆頂的堆裡面最小,但不能說明從這個節點就向上尋找就最大 //這裡需要自底向上再來一次調整 if (array[i] == moved) { if (cmp == null) siftUpComparable(i, moved, array); else siftUpUsingComparator(i, moved, array, cmp); } } size = n;}
remove的時候有2個調整,先自頂向下調整,保證最小,然後再向上調整。
出處:http://blog.csdn.net/xiaoxufox/article/details/51860543