PriorityBlockingQueue
是一个带有优先级的阻塞队列。
基本原理和前面介绍的ArrayBlockingQueue
类似。
在看这篇文章之前,你可以先仔细看下这篇文章——ArrayBlockingQueue源码解析
PriorityBlockingQueue和ArrayBlockingQueue一样,初始化时,可指定队列的大小。
不同的是,前者可以自动扩容,而后者不会。
另一个很大不同是,前者可以实现优先出队,后者则是先进先出。
比如有这么一个场景,一个市场上陆陆续续来了很多卖苹果的。
这时来了一个大买家,说谁最便宜就买谁的。 先来后来无所谓。
static class SaleApple implements Comparable<SaleApple> {
int price; // 苹果的价格
String brand; // 草果的品种
SaleApple(int price, String brand){
this.price = price;
this.brand = brand;
}
@Override
public int compareTo(SaleApple o) {
return this.price - ((SaleApple) o).price;
}
}
public static void main(String[] args) throws Exception {
PriorityBlockingQueue<SaleApple> priorityQueue = new PriorityBlockingQueue<>(3);
Thread t1 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
for (int i = 0; i < 10; i++) {
SaleApple apple = new SaleApple(RandomUtil.randomInt(5,50), "one_brandNo" + i);
boolean offer = priorityQueue.offer(apple);
log.info("one_offer:{}, value:{}", offer, apple.price);
}
}
}, "t1");
t1.start();
Thread.sleep(3000);
SaleApple apple = priorityQueue.take();
log.info(" price:{}, brand:{}", apple.price, apple.brand);
}
上面是一个小demo,PriorityBlockingQueue 就特别适合这样的场景,出队的就是最便宜的那个。
PriorityBlockingQueue 底层是利用 PriorityQueue
, 默认是小顶堆。
堆这种数据结构如果不熟悉,可以看我之前的博客——数据结构:堆(小顶堆和大顶堆)。
好,现在开始一点一点分析源码
一、初始化
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];
}
在本例中comparator是null, demo 中 SaleApple 已实现了Comparable接口。
/**
* 队列的默认初始容量
*/
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;
/**
* Condition对象
*/
private final Condition notEmpty;
/**
* 扩容时用的CAS锁标记
*/
private transient volatile int allocationSpinLock;
/**
* 优先级队列
*/
private PriorityQueue<E> q;
初始化代码很简单,用一张图来形象说明下。
二、入队源码
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(n, e, array)
这个方法。
我先画图来说明入队的操作,然后再来看入队的源码
图上画的是一个简单的小顶堆。数据是放在下面那个数组中。
其中子节点与父节点,下标有这么一个关系。(子节点下标 - 1 )/2 = 父节点下标。
比如数字8的下标是 5, 那 (5-1) / 2 = 2,就是其父节点的下标。
现在往这个堆中插入元素 1,
先将 1 放在最后一个位置,然后调整二叉树,直到其满足小顶堆的特性,即堆化
1放到数组最后,与父节点4比较大小,小于父节点,就交换位置,再进行下一轮比较
1与父节点2比较,小于父节点,与父节点位置进行交换。此时1是根节点,比较结束。
堆 这种数据结构,增删元素的时间复杂度是 O(logn) ,堆化的操作,保证堆结构没有被破坏。
理解了这几张图,再来看源码
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; // 找到位置后赋值
}
再看下扩容的源码 tryGrow(array, cap)
,扩容的条件是, size == queue.length
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 {
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) // 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);
}
}
先解锁,然后CAS 操作,allocationSpinLock 由0设置为1,成功则扩容。
原数组小于 64,那就扩容一倍, 否则扩容 50%。
最后再抢锁,将扩容后的数组覆盖原数组。
三、出队源码解析
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;
}
notEmpty.await()
、notEmpty.signal()
这两个方法,之前的博文有详细说明。
这里不再讲了,如果不清楚的,看这篇——await 和 signal
dequeue()
过程,差不多就是入队操作反过来。
将堆顶元素去掉,然后,把数组最后一个放堆顶,再进行堆化。
还是看图吧,图容易理解。
1 出队,然后把最后一个元素4,放到 1 的位置,然后进行堆化
4 的两个子节点,取较小的 2。 4 大于 2,交换位置。此时,4 比其子节点小,堆化结束。
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; // 将最后一个元素设置为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) {
Comparable<? super T> key = (Comparable<? super T>)x;
// n 是元素总个数,n 后一半,是子节点,这个自己画几个二叉树算下就明白了。
int half = n >>> 1;
while (k < half) { // 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]; // c 是取较小的那个子节点
if (key.compareTo((T) c) <= 0) // 如果父节点比 c 小,停止堆化
break;
array[k] = c; // 父节点比c大,那交换位置
k = child; // 下标盯住调换后的位置
}
array[k] = key;
}
}
结合图,还有代码的注释,出队的源码就解析完了。
四、总结
PriorityBlockingQueue
是具有优先级的阻塞队列。初始容量是11,能自动扩容。
默认是小顶堆,出队永远是最小值,反之大顶堆,出队的是最大值。
与ArrayBlockingQueue
相比,ArrayBlockingQueue 是先进先出,初始化需要指定容量,无法自动扩容。
与LinkedBlockingQueue
相比,LinkedBlockingQueue 是 先进先出,入队出锁是两把不同的两把锁,底层是链表,不存在所谓的扩容不扩容。
这三个阻塞队列,都采用notEmpty.await()
、notEmpty.signal()
来阻塞与唤醒。
相关文章: