PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。但需要注意的是不能保证同优先级元素的顺序。
PriorityBlockingQueue有四个构造方法:
// 默认的构造方法,该方法会调用this(DEFAULT_INITIAL_CAPACITY, null),即默认的容量是11
public PriorityBlockingQueue()
// 根据initialCapacity来设置队列的初始容量
public PriorityBlockingQueue(int initialCapacity)
// 根据initialCapacity来设置队列的初始容量,并根据comparator对象来对数据进行排序
public PriorityBlockingQueue(int initialCapacity, Comparator super E> comparator)
// 根据集合来创建队列
public PriorityBlockingQueue(Collection extends E> c)
PriorityBlockingQueue类定义为:
public class PriorityBlockingQueue extends AbstractQueue implements BlockingQueue, java.io.Serializable
该类同样继承了AbstractQueue抽象类并实现了BlockingQueue接口,这里不再叙述。
PriorityBlockingQueue内部是采用二叉堆来实现的,这里不再解释,同时,该类使用ReentrantLock和Condition来确保多线程环境下的同步问题。
// 默认容量
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;
// 自旋锁
private transient volatile int allocationSpinLock;
// 为了兼容之前的版本,只有在序列化和反序列化才非空
private PriorityQueue q;
我们前面介绍的ArrayBlockingQueue和LinkedBlockingQueue都是具有notEmpty和notFull两个Condition,那PriorityBlockingQueue为什么只有一个notEmpty呢?因为PriorityBlockingQueue是一个无界阻塞队列,可以一直向队列中插入元素,除非系统资源耗尽,所以该队列也就不需要notFull了。
我们来看一下add(E e)方法:
public boolean add(E e) {
return offer(e);
}
该方法很简单,其内部调用了offer(E e)方法:
public boolean offer(E e) {
// 若插入的元素为null,则直接抛出NullPointerException异常
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;
}
我们先看一看扩容操作:
private void tryGrow(Object[] array, int oldCap) {
// 释放全局锁
lock.unlock(); // must release and then re-acquire main lock
Object[] newArray = null;
// 使用CAS操作来修改allocationSpinLock
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {
try {
// 容量越小增长得越快,若容量小于64,则新容量是oldCap * 2 + 2,否则是oldCap * 1.5
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表明已经有其他线程对数组进行了扩容操作,让出CPU(因为扩容线程后续还有其他的操作)
if (newArray == null) // back off if another thread is allocating
Thread.yield();
// 获取全局独占锁
lock.lock();
// 若queue没有发生变化
if (newArray != null && queue == array) {
// 将创建的新数组赋值给queue
queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
在扩容操作开始时,线程释放了全局锁,这是为什么呢?其实这也是为了更好的并发性,虽然释放了全局锁,但后面扩容操作是通过简单的乐观锁allocationSpinLock来进行控制的。
offer(E e)方法中还有两个比较重要的方法是siftUpComparable和siftUpUsingComparator,我们先来看一下siftUpComparable方法:
private static void siftUpComparable(int k, T x, Object[] array) {
Comparable super T> key = (Comparable super T>) x;
// k是否达到二叉树的顶点
while (k > 0) {
// 计算父节点的下标
int parent = (k - 1) >>> 1;
Object e = array[parent];
// key >= e说明已经按照升序排序,跳出循环
if (key.compareTo((T) e) >= 0)
break;
// 否则,将parent节点的值放在子节点上
array[k] = e;
// 将parent当作下次比较的k
k = parent;
}
// 将值key放置合适的位置上
array[k] = key;
}
该方法的主要操作就是,将x放到位置k,然后调整x的位置,直到它大于等于父节点。
siftUpUsingComparator与siftUpComparable的唯一不同点在于,siftUpUsingComparator使用自定义的比较器来比较元素,其余操作相同。
我们来看一下poll()方法,该方法每次都是返回数组下标为0的元素:
public E poll() {
// 获取全局锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
return dequeue();
} finally {
// 释放全局锁
lock.unlock();
}
}
出队操作其实是由dequeue()方法来完成的:
private E dequeue() {
int n = size - 1;
// 队列是否为空
if (n < 0)
return null;
else {
Object[] array = queue;
// 取出第一个元素,作为返回值
E result = (E) array[0];
// 取出最后一个元素,然后将数组最后一个元素设置为null
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;
}
}
重新调整二叉树的操作是由siftDownComparable方法和siftDownUsingComparator方法来完成的,我们先看一下siftDownComparable方法:
private static 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; // loop while a non-leaf
// k其实是half的父节点下标,若k >= half,则表明k在数组中已经没有子节点
while (k < half) {
// 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];
// key <= c说明已经按照升序排序,跳出循环
if (key.compareTo((T) c) <= 0)
break;
// 否则,将子节点的值放在父节点上
array[k] = c;
// 将child当作下次比较的k
k = child;
}
// 将值key放置合适的位置上
array[k] = key;
}
}
该方法的主要操作就是,将x放到位置k,然后调整x的位置,直到它小于等于子节点。
siftDownUsingComparator与siftDownComparable的唯一不同点在于,siftDownUsingComparator使用自定义的比较器来比较元素,其余操作相同。
Java并发编程之ArrayBlockingQueue阻塞队列详解
Java并发编程之ReentrantLock详解
Java并发编程之Condition详解
方腾飞:《Java并发编程的艺术》