简介
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源码详解
PriorityBlockingQueue类定义为:
public class PriorityBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, 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<E> 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 <T> 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 <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; // 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并发编程的艺术》