挖掘队列源码之PriorityBlockingQueue
前言
探索PriorityBlockingQueue是基于JDK1.8,它是基于二叉堆的无界阻塞队列,二叉堆又可以分成最大堆与最小堆,很显然,既然是优先级队列,那么优先级高者自然比优先级低者优先出队列,PriorityBlockingQueue采用的是数值越小优先级越高,即最小堆,利用每个元素的优先级进行排序,所以这不在是按照先进先出顺序了!对于无界来说,就不会存在队列饱满的情况了,它会进行扩容,自然也就不会发生阻塞的情况了,这是与ArrayBlockingQueue/LinkedBlockingQueue最大的区别了,不过在空队列的情况下仍然会发生阻塞直到出现新元素才会被唤醒继续执行。对于优先级队列而言,自然是在添加的时候比较每个元素的优先级高低,所以它要求每个元素必须继承Comparable或自定义比较器,更不能添加null。最后在说一点,接下来的内容将不会着重介绍二叉堆,它较为简单且之前的文章也简单介绍过,所以读者可以先了解下,有助于理解其中的方法,接下来进入主题。
数据结构
public class PriorityBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
// 其二叉堆的数据结构是数组,数组的默认初始容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
/**
* 我们都知道定义一个数组的大小是 int 类型,那么也就意味着最大的数组大小应该是Integer.MAX_VALUE,但是这里为啥要减去8呢?
* 查阅资源发现大部分的人都在说8个字节是用来存储数组的大小,半信半疑
* 分配最大数组,某些VM会在数组中存储header word,按照上面的说法指的应该是数组的大小
* 若尝试去分配更大的数组可能会造成 OutOfMemoryError: 定义的数组大小超过VM上限
* 不同的操作系统对于不同的JDK可能分配的内存会有所差异,所以8这个数字可能是为了保险起见
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 存储元素
private transient Object[] queue;
// 队列中元素的个数
private transient int size;
// 自定义比较器,若为null则要求元素继承Comparable进行比较
private transient Comparator<? super E> comparator;
// 可重入锁,防止多线程并发访问队列
private final ReentrantLock lock;
// 从空队列中获取元素导致阻塞直到新元素插入时被唤醒
private final Condition notEmpty;
// 队列进行扩容时的标志,只会有一个线程扩容成功,扩容的前提是已经获取到锁了,那么在过程中会先释放其锁,让其他线程可以尽早获取元素,也就是说扩容与获取元素可以同时操作
private transient volatile int allocationSpinLock;
}
构造函数
/**
* 初始化,采用默认数组容量大小
*/
public PriorityBlockingQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
/**
* 初始化,自定义数组容量大小
* @param initialCapacity 数组容量大小
*/
public PriorityBlockingQueue(int initialCapacity) {
this(initialCapacity, null);
}
/**
* 初始化,自定义数组容量大小与比较器
* @param initialCapacity 数组容量大小
* @param comparator 自定义比较器
*/
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];
}
/**
* 将集合中的元素放入到队列
* 若指定集合是PriorityBlockingQueue,那么最终队列的顺序将会和集合一致,若是其他集合类,则最终队列将会进行排序
* @param c 集合
*/
public PriorityBlockingQueue(Collection<? extends E> c) {
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
boolean heapify = true; // 如果集合未排序则标志为true,若c是PriorityBlockingQueu或SortedSet,说明已经排过序了
boolean screen = true; // 是否要判空的标志,若c本身就是PriorityBlockingQueue,就不需要判空,因为它本身就不允许添加null,而对于其他集合类来说是允许添加null
if (c instanceof SortedSet<?>) {
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
this.comparator = (Comparator<? super E>) ss.comparator();
heapify = false;
}
else if (c instanceof PriorityBlockingQueue<?>) {
PriorityBlockingQueue<? extends E> pq = (PriorityBlockingQueue<? extends E>) c;
this.comparator = (Comparator<? super E>) pq.comparator();
screen = false;
if (pq.getClass() == PriorityBlockingQueue.class) // exact match
heapify = false;
}
Object[] a = c.toArray();
int n = a.length;
if (a.getClass() != Object[].class) // 构建数组
a = Arrays.copyOf(a, n, Object[].class);
if (screen && (n == 1 || this.comparator != null)) { // 判空操作
for (int i = 0; i < n; ++i)
if (a[i] == null)
throw new NullPointerException();
}
this.queue = a;
this.size = n;
if (heapify)
heapify(); // 对队列进行排序
}
简单方法
/**
* 数组扩容
* 调用该方法的前提是已经获取了锁
* oldCap > 64的情况是以50%的速率进行扩容
* @param array 旧数组
* @param oldCap 旧数组的容量大小
*/
private void tryGrow(Object[] array, int oldCap) {
lock.unlock(); // 调用此方法的前提是已经获取了锁,而这先是释放锁让其他线程可以去获取元素
Object[] newArray = null;
if (allocationSpinLock == 0 && UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset, 0, 1)) {
try {
int newCap = oldCap + ((oldCap < 64) ? (oldCap + 2) : (oldCap >> 1)); // 计算新数组的容量大小,oldCap > 64的情况是以50%的速率进行扩容
if (newCap - MAX_ARRAY_SIZE > 0) { // 防止溢出
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说明其他的线程也在调用扩容方法,不过有一个线程在扩容就可以了,所以让其放弃线程调度
Thread.yield();
lock.lock(); // 由于新数组已经构建完成了,现在需要赋值了,为了避免多线程的干扰需要先上锁
if (newArray != null && queue == array) {
queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
/**
* 出队列
* @return 元素
*/
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;
}
}
/**
* 上浮插入的元素以满足二叉堆的性质
* 二叉堆的性质:PriorityBlockingQueue采用的是最小堆,即父元素的数值比子元素的数值要小
*
* 插入的元素的操作步骤一般如下:
* 1. 将插入的元素放到数组的末尾
* 2. 由于需要父元素的数组比子元素的数值要小,故要与父元素进行比较
* 3. 若插入的元素比父元素小,则将插入的元素与父元素进行值交换,在交换后它又有了新的父元素,故而需要继续往上比较,直到堆顶或不在小于父元素,相当于在重复步骤2
* 4. 若插入的元素比父元素大,直接结束
*
* 假设一个元素的索引为:N,其父元素的索引为:(N - 1)/2,左子元素的索引为:2N + 1,其右子元素的索引为:2N + 2
* @param k 插入的元素的索引
* @param x 插入的元素
* @param array 数组
*/
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;
}
/**
* 同上,唯一的差别是在比较器上
* @param k 插入的元素的索引
* @param x 插入的元素
* @param array 数组
* @param cmp 比较器
*/
private 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;
}
/**
* 下沉元素以满足二叉堆的性质,下沉就是拿指定元素与其左右子元素进行比较
* 思路:
* - 不管移除的是哪个元素,拿数组末尾的元素是最少成本的,因为拿该元素的值去覆盖移除元素的值来使其还是一颗完全二叉树,所以最终只要让其满足二叉堆性质就可以了
* - 针对移除元素来说,就相当于把移除元素的位置空出来了,因为是二叉堆要满足其性质,所以就要考虑是它的子节点还是末尾的节点更适合来做移除元素的位置(最大堆/最小堆)
* - 因为末尾元素最终都会被移动到指定位置,故而先将末尾位置置null
*
* 针对要移除的元素分为两种情况(最小堆):
* 1. 移除的元素无子元素,也就是说是叶子节点,如下步骤:
* 11. 直接将末尾的元素的值覆盖,同时将末尾位置设置成null,因为它并无子元素,所以不用考虑是否比子元素大,但是有一点要考虑是覆盖完后是否比父元素还要小
* 12. 若比父元素还要小的话就要做上浮操作,即调用siftUpUsingComparator/siftUpComparable即可,最后退出
*
* 2. 删除的元素有子元素,如下步骤:
* 21. 先比较两个子元素的值的大小,获取值最小的元素
* 22. 在将值最小的元素与末尾的元素进行比较(若只有一个子元素的话,那么只能是左子元素,就直接比较大小)
* 23. 若是末尾的元素更小的话,那么直接覆盖移除的元素的值即可,同时将末尾位置设置成null,最后退出
* 24. 若是其子元素更小的话,那么用其值最小的元素覆盖到移除的元素的值,此时值最小的元素的位置就空出来了(相当于此时它被移除了),那么此时重复步骤12
*
* 假设一个元素的索引为:N,其父元素的索引为:(N - 1)/2,左子元素的索引为:2N + 1,其右子元素的索引为:2N + 2
* @param k 移除/指定元素的索引
* @param x 末尾/指定元素
* @param array 数组
* @param 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; // 通过该值来判断移除节点是否有子元素,可以画几个例子验证下,我也不懂怎么来的,只能说写算法的人牛逼
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) // 末尾元素与值最小的元素进行比较
break;
array[k] = c;
k = child;
}
array[k] = key;
}
}
/**
* 同上,唯一的差别是在比较器上
* @param k 移除/指定元素的索引
* @param x 末尾/指定元素
* @param array 数组
* @param n 数组的容量大小
* @param cmp 比较器
*/
private static <T> void siftDownUsingComparator(int k, T x, Object[] array, int n, Comparator<? super T> cmp) {
if (n > 0) {
int half = n >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = array[child];
int right = child + 1;
if (right < n && cmp.compare((T) c, (T) array[right]) > 0)
c = array[child = right];
if (cmp.compare(x, (T) c) <= 0)
break;
array[k] = c;
k = child;
}
array[k] = x;
}
}
/**
* 构建二叉堆,实际上就是排序
*/
private void heapify() {
Object[] array = queue;
int n = size;
int half = (n >>> 1) - 1;
Comparator<? super E> cmp = comparator;
if (cmp == null) { // 涉及到算法的内容我也不懂...
for (int i = half; i >= 0; i--)
siftDownComparable(i, (E) array[i], array, n);
}
else {
for (int i = half; i >= 0; i--)
siftDownUsingComparator(i, (E) array[i], array, n, cmp);
}
}
/**
* 插入元素
* 当队列饱满的情况下会发生扩容
* @param e 元素
* @param timeout 指定时间
* @param unit 时间单位
* @return 结果值
*/
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;
}
/**
* 获取元素
* 当队列为空的情况下直接返回null
* @return 结果值
*/
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return dequeue();
} finally {
lock.unlock();
}
}
/**
* 获取元素
* 当队列为空的情况下一直阻塞等待,直到被唤醒或被中断
* @return 结果值
*/
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;
}
/**
* 获取元素
* 当队列为空的情况下阻塞等待指定时间,直到被唤醒或被中断或超时
* @param timeout 指定时间
* @param unit 时间单位
* @return 结果值
*/
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;
}
/**
* 获取元素
* 与前面的三个方法较大不同,因为其不会导致元素出队列,所以该方法即使调用多次依然能获取到元素,而其他三个方法则不行
* @return 结果值
*/
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (size == 0) ? null : (E) queue[0];
} finally {
lock.unlock();
}
}
/**
* 移除指定元素
* @param o 指定元素
* @return 是否移除成功
*/
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();
}
}
总结
PriorityBlockingQueue虽然提供了多个插入元素的方法,但实际上都调用的同一个方法,所以在队列饱满的情况下插入新元素会发生扩容从而继续执行,也就是不会发生阻塞;
-
在队列为空的情况下
获取元素-
poll:直接返回null。
-
take:一直被阻塞等待直到被唤醒或线程被中断。
-
poll(time):阻塞等待直到被唤醒或线程被中断或
超时。 -
peek:直接返回null,该方法即使调用多次依然能获取到元素,而其余3个方法则不行。
-
PriorityBlockingQueue最大的特点就是无界及二叉堆,关于二叉堆最好是能理解上浮与下沉的代码,说不定以后面试会考呢!
浙公网安备 33010602011771号