PriorityBlockingQueue简介
它是一个数组实现的带优先级无阻塞队列并发安全队列。
PriorityBlockingQueue的特性
1.PriorityBlockingQueue内部是一个数组queue,但是其实数据结构是使用数组实现的一个最小堆,压入队列时需要计算最小堆,弹出队列时需要重新调整根节点。
2.带优先级的队列(带排序功能)。
3.无界队列,默认队列大小为11,当队列满了之后会自动扩容(和ArrayList类似的扩容数组)。
4.take队列为空时阻塞.但是PriorityBlockingQueue队列是无界的,put方法不存在队列满的时候阻塞的情况,所以put方法是不阻塞的,可以说PriorityBlockingQueue是一个半阻塞的队列。
5.和ArrayBlockingQueue类似,是独占锁来控制的, 就是说多线程访问时只能有一个线程可以进行入队或出队操作。
6.PriorityBlockingQueue虽然是无界的,但是最大长度只能为Integer.MAX_VALUE - 8,并不是说无界了就可以任意长度,毕竟它是通过数组实现的,数组的最大值只能为Integer.MAX_VALUE。
入队put方法,内部调用offer(e)
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 {
// 如果没有比较器,则元素e必须实现Comparable接口
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftUpComparable(n, e, array);
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
// 此处唤醒调用take时队列为空的阻塞线程
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}
队列满如何进行扩容?
// 扩容的时候先释放锁,如果take的线程获取了锁可以取,如果offer的线程获取了锁可以放
// (方法中释放了锁,别的线程就可以进去这个方法,也可以进去其它需要锁的方法)
private void tryGrow(Object[] array, int oldCap) {
lock.unlock(); // 释放锁,执行take的线程可以竞争锁,正常出队(保证性能)
Object[] newArray = null;
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {// 此处使用cas,保证只有一个线程能进行扩容
try {
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // 如果队列容量低于64,则扩容后为2倍原容量+2
(oldCap >> 1));// 否则扩容为1.5倍原容量
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 {// 此处不需要cas操作,因为此处只有一个线程操作allocationSpinLock
allocationSpinLock = 0;
}
}
// 失败扩容的线程newArray == null,调用Thread.yield()让出cpu, 让扩容线程扩容后优先调用lock.lock重新获取锁,
// 但是这得不到一定的保证,有可能调用Thread.yield()的线程先获取了锁。
if (newArray == null)
Thread.yield();
lock.lock(); // 此处获取锁,可能是扩容线程,也可能是其它线程
// 如果扩容线程获取到了锁,则能成功给共享变量赋值,如果不是,则根本进入不了下面的if代码块
// 扩容线程newArray != null, 而其它线程newArray = null,至于其它线程为什么newArray为空,这是跟线程调用栈相关内容了。
if (newArray != null && queue == array) {
queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
入队元素调整(节点上浮)
siftUpComparable,siftUpUsingComparator这两个方法比较简单,简单说下之间的区别。
siftUpComparable方法添加的元素必须实现Comparable接口,通过元素自身的compareTo方法比较。
siftUpUsingComparator方法通过初始化时传入的comparator来比较。
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
@SuppressWarnings("unchecked")
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
@SuppressWarnings("unchecked")
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
构建最小堆过程
如果从一个优先级队列构造一个新的优先级队列,此时内部的数组元素不需要进行调整,只需要将原数组元素都复制过来即可。 (PriorityBlockingQueue(Collection<? extends E> c)构造器,如果传入的集合是SortedSet,PriorityBlockingQueue对象,不需要堆化操作) 但是从其它非PriorityQueue的集合中构造优先级队列时,需要先将元素复制过来后再进行调整,此时调用的是heapify方法。
private void heapify() {
// 从最后一个非叶子节点开始从下往上调整,最后非叶子节点获取下标(size >>> 1) - 1
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
// 这个函数即对应上面的元素删除时从上往下调整的步骤
private void siftDown(int k, E x) {
if (comparator != null)
// 如果比较器不为null,则使用比较器进行比较
siftDownUsingComparator(k, x);
else
// 否则使用元素的compareTo方法进行比较
siftDownComparable(k, x);
}
private void siftDownUsingComparator(int k, E x) {
// 使用half记录队列size的一半,如果比half小的话,说明不是叶子节点
// 因为最后一个节点的序号为size - 1,其父节点的序号为(size - 2) / 2或者(size - 3 ) / 2
// 所以half所在位置刚好是第一个叶子节点
int half = size >>> 1;
while (k < half) {
// 如果不是叶子节点,找出其孩子中较小的那个并用其替换
int child = (k << 1) + 1; // 找出k下标所在节点的左子节点索引
Object c = queue[child]; // 左子节点的值
int right = child + 1; // 右子节点索引
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0) // 子节点较小的跟父节点比较
break;
queue[k] = c; // 替换索引k所在元素值
k = child;
}
queue[k] = x;
}
// 同上,只是比较的时候使用的是元素的compareTo方法
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
int half = size >>> 1; // 如果是非叶子节点则继续循环
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
if (key.compareTo((E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = key;
}
siftDown方法是这里面比较重要的方法之一,有两个参数,一个是序号k,另一个是元素x,这个方法的作用是把x从k开始往下调整,使得在节点k在其子树的每相邻层中,父节点都小于其子节点。所以heapify的作用就比较明显了,从最后一个非叶子节点开始,从下往上依次调整其子树,使得最终得到的树里,根节点是最小的。这里要先理解一下为什么heapify中i的初始值要设置为(size >>> 1) - 1。因为这是最后一个非叶子节点的位置,不信的话可以随便画几个图验证一下,至于siftDownUsingComparator方法中,int half = size >>> 1;这里half则是第一个叶子节点的位置,小于这个序号的节点都是非叶子节点,结合下图来验证。
以下方数组为例
下标4的节点需要进行一轮调整,siftDown(4,queue[4]),此时while(k<half)中k=10,退出while循环。
下标1的节点需要进行两轮调整,siftDown(1,queue[1]),3-7互换,第二轮7小于左右子节点,直接结束。
下标0的节点需要进行三轮调整,siftDown(0,queue[0]),第一次14-3互换,此时while(k<half)中k=1,第二次14-6互换,此时while(k<half)中k=3,第三轮14-10互换,此时while(k<half)中k=8,退出while循环,最小堆构造完成。
移除最小堆指定元素
当移除的不是堆顶元素的时候,同样先用最后一个元素代替,然后先从被移除的位置开始向下调整,如果发现没有改动,说明子节点都比它大,则再向上调整。
// 这里不是移除堆顶元素,而是移除指定元素
public boolean remove(Object o) {
// 先找到该元素的位置
int i = indexOf(o);
if (i == -1)
return false;
else {
removeAt(i);
return true;
}
}
// 移除指定序号的元素
private E removeAt(int i) {
// assert i >= 0 && i < size;
modCount++;
// s为最后一个元素的序号
int s = --size;
if (s == i)
queue[i] = null;
else {
// moved记录最后一个元素的值
E moved = (E) queue[s];
queue[s] = null;
// 用最后一个元素代替要移除的元素,并向下进行调整
siftDown(i, moved);
// 如果向下调整后发现moved还在该位置,则再向上进行调整
if (queue[i] == moved) { // 此时说明子节点都比它大
siftUp(i, moved);
if (queue[i] != moved)
return moved;
}
}
return null;
}
出队过程
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 加锁(可响应中断)
lock.lockInterruptibly();
E result;
try {
// 如果队列为空,take 方法会阻塞出队线程
while ( (result = dequeue()) == null)
/**
* 如果队列中没有元素,会阻塞后续调用 take 方法出队的线程
* 直到队列添加了元素后唤醒 notEmpty,才可以继续执行
*/
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];
// 把最后一个元素置 null,因为要把它放到堆顶,向下逐步调整堆结构
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;
}
}