PriorityBlockingQueue优先级队列,内部使用了二叉堆的数据结构来存储数据,根据优先级对队列中的元素进行排序,然后出队的时候就会按照一定的顺序来出队
话不多说,先来看看PriorityBlockingQueue构造方法的源码
public PriorityBlockingQueue() {
// 默认的构造函数传入了一个初始容量,是11
// 第二个参数是Comparator,不传就使用默认的
this(DEFAULT_INITIAL_CAPACITY, null);
}
public PriorityBlockingQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
// 实现并发安全的锁
this.lock = new ReentrantLock();
// 如果队列为空时,阻塞用的Condition
this.notEmpty = lock.newCondition();
this.comparator = comparator;
// 初始化一个初始容量的数组作为队列
this.queue = new Object[initialCapacity];
}
接下来我们来看看入队的操作
public void put(E e) {
// 调用offer方法
offer(e); // never need to block
}
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)
// 如果我们不传Comparator就使用默认的
siftUpComparable(n, e, array);
else
// 使用自己的Comparator
siftUpUsingComparator(n, e, array, cmp);
// 队列元素数量加1
size = n + 1;
// 当我们入队成功之后,说明队列中已经有元素了,则唤醒被notEmpty阻塞的线程,也就是可以继续执行出队操作了
notEmpty.signal();
} finally {
// 释放锁
lock.unlock();
}
return true;
}
下面我们具体来分析一下自带Comparator的siftUpComparable方法,但是在这之前,我们需要了解一些二叉堆的特性,假如我们有一个数组[1,2,3,4,5,6,7],那么在二叉堆中是什么样子的呢
二叉堆有一个特性,根据数组的下标,如果节点的下标位置在n处,那么其左孩子节点下标为:2 * n + 1 ,其右孩子节点下标为2 * (n + 1),其父节点下标为(n - 1) / 2 处,对应到数组的话如下图,我用不同的颜色标注了不同的节点
再来看看两种不同的二叉树添加元素的方式,首先我们先假设数组只有[1,2,3,4,5,6],现在要把7添加进来,通过(n - 1) / 2找到父节点的值是3,发现 7 > 3,那么直接将7加在数组尾部即可,就变成了我们上面所画的图,这种添加元素方式只要一遍操作就可以完成
第二种方式,我们先假设数组是[1,2,4,5,6,7],那么对应的二叉树就是这样的
现在我们想把3加入进来,首先是将3加入到数组尾部,数组变为[1,2,4,5,6,7,3],然后找到父节点是4,我们发现 3 < 4,此时就将3和4的位置进行调换,此时数组是 [1,2,3,5,6,7,4],二叉树变成了
这个时候再将3与父节点的值比较,发现 3 > 1,此时就已经完成了二叉堆的平衡了,虽然跟我们最开始的二叉堆不太一样,但是这也是一个二叉堆,这个堆的父节点都是小于任意一个子节点的,所以这是一个最小堆,反之父节点都大于任意一个子节点的话,那么就是最大堆了,我们可以发现添加元素的时候,其实就是将要添加的元素与父节点比大小,大的话则不用改变,如果小的话,则交换位置,也就是所谓的上浮,有点像冒泡,将小的值与之前的更大值交换;那么删除操作就是反过来的下沉了
还是用最初的二叉堆数组[1,2,3,4,5,6,7],删除的话,其实就是删除根节点,假如我们要将1删除,那么我们就需要将数组的最后一个元素与根节点交换位置,然后不断的与子节点中较小的元素比较,如果子节点更小的话,那么就不断下沉,直到沉到底或者有子节点更大为止
了解过了二叉堆,我们再来看siftUpComparable方法
// k为队列中元素的数量,x为要入队的元素,array就是我们的队列
private static <T> void siftUpComparable(int k, T x, Object[] array) {
Comparable<? super T> key = (Comparable<? super T>) x;
while (k > 0) {
// 这里拓展一下位运算,<<表示左移移,不分正负数,低位补0
// >>表示右移,如果该数为正,则高位补0,若为负数,则高位补1
// >>>表示无符号右移,也叫逻辑右移,即若该数为正,则高位补0,而若该数为负数,则右移后高位同样补0
// 这里的意思等价于parent = (k - 1) / 2,通过上面的二叉堆特性,我们知道了这个parent其实就是k的父节点
int parent = (k - 1) >>> 1;
// 取出队列中间的元素,设置为e
Object e = array[parent];
// 如果要入队的元素大于等于e的话,就直接break掉,最后将这个元素加在数组最后即可
if (key.compareTo((T) e) >= 0)
break;
// 如果上面的if条件不满足,则将e与k的位置交换,然后再跟父节点比大小
array[k] = e;
k = parent;
}
array[k] = key;
}
看完源码之后,我们发现siftUpComparable方法其实就是二叉堆的添加元素的方法实现,所以PriorityBlockingQueue的入队其实就是将元素放入了一个二叉堆中,然后利用二叉堆来进行排序的
接下来看看出队的take方法
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
// 调用dequeue出队
while ( (result = dequeue()) == null)
// 如果队列中已经没有元素了,那么就调用notEmpty的await方法,阻塞线程继续出队
notEmpty.await();
} finally {
lock.unlock();
}
return result;
}
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方法
siftDownComparable(0, x, array, n);
else
siftDownUsingComparator(0, x, array, n, cmp);
size = n;
return result;
}
}
// 这里跟siftUpComparable方法相比多了一个参数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;
// half = n / 2
int half = n >>> 1; // loop while a non-leaf
while (k < half) {
// child = 2 * k + 1,获取左孩子节点下标
int child = (k << 1) + 1; // assume left child is least
Object c = array[child];
// right = child + 1 = 2 * (k + 1),获取右孩子节点下标
int right = child + 1;
// 如果右孩子节点下标小于数组长度且左孩子节点的值大于右孩子节点的值,那么将右孩子节点的值赋值给了c
// 这么做是因为二叉堆左右孩子节点的值是不区分谁大谁小的,这一步就得到了两个孩子节点中较小的那个节点的值
if (right < n &&
((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
c = array[child = right];
// 如果要删除的节点小于较小子节点的值,则break掉
if (key.compareTo((T) c) <= 0)
break;
// 否则继续下沉
array[k] = c;
k = child;
}
array[k] = key;
}
}
通过上面出队入队的源码分析,我们知道了PriorityBlockingQueue实现并发安全的机制其实就是在出入队操作时加了一个ReentrantLock,并且使用的是同一个lock,说明出入队操作之间并不能同时执行,只能有一个操作获取锁;然后在出队的时候,如果队列中的元素为空的话,则有一个Condition会将出队的线程阻塞住,阻止继续出队了,只有当入队操作入队成功唤醒了这个Condition才能继续出队
PriorityBlockingQueue用来实现排序功能的原理其实就是底层使用了二叉堆来存储队列中的元素,出入队的操作都会将元素放入二叉堆中进行排序,这样的话,在出队的时候,我们就可以获得排序过的元素了