PriorityBlockingQueue是一个阻塞队列,它是有界的,有最大容量,一般达不到,虽然它是队列,它并不能满足先进先出规则,它是按优先级出队,它会将插入的数据进行排序,按优先级出队,底层采用数组实现的二叉堆,所以当我们往队列中添加自定义对象时,自定义对象需要实现Comparable接口,也就是需要告诉队列,你的数据需要按什么规则进行排序。具体情况,我们根据源码来解释
1、重要属性
// 默认容量
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;
// 因为PriorityBlockingQueue的底层是基于二叉堆的,而二叉堆又是基于数组实现的,
// 数组长度是固定的,如果需要扩容,需要构建一个新数组。PriorityBlockingQueue在做扩容操作时,
// 不会lock住的,释放lock锁,基于allocationSpinLock属性做标记,来避免出现并发扩容的问题。
private transient volatile int allocationSpinLock;
2、构造方法
// 无参构造
public PriorityBlockingQueue() {
// 容量默认
this(DEFAULT_INITIAL_CAPACITY, null);
}
// 带容量的构造
public PriorityBlockingQueue(int initialCapacity) {
this(initialCapacity, null);
}
// 带容量和比较器的构造方法
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];
}
// 传入集合作为构造参数,表示要将集合中的数据放入队列
public PriorityBlockingQueue(Collection<? extends E> c) {
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
boolean heapify = true; // true if not known to be in heap order
boolean screen = true; // true if must screen for nulls
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 c.toArray incorrectly doesn't return Object[], copy it.
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();
}
3、写入操作
3.1 add(E e)
add方法没啥好说的,最终掉的是offer
public boolean add(E e) {
return offer(e);
}
3.2 offer(E e)
此方法往队列中添加元素,成功返回true,虽然是有最大容量限制,但是几乎是达不到最大容量的,相当于无界队列,所以生产者是用不着阻塞的,最多是等待别的线程扩容
public boolean offer(E e) {
// 元素为空 抛异常
if (e == null)
throw new NullPointerException();
// 加锁
final ReentrantLock lock = this.lock;
lock.lock();
int n, cap;
Object[] array;
// 在添加数据之前,会采用while循环的方式,来判断当前元素个数是否大于等于数组长度。
// 如果满足,需要执行tryGrow方法,对数组进行扩容
// 如果两个线程同时执行tryGrow,只会有一个线程在扩容,另一个线程可能多次走while循环,
// 多次走tryGrow方法,但是依然需要等待前面的线程扩容完毕
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);
// 元素个数加1
size = n + 1;
// 唤醒等待的读线程
notEmpty.signal();
} finally {
// 释放锁
lock.unlock();
}
return true;
}
扩容方法,扩容前先释放锁资源,由于底层是数组结构,扩容需要申请新数组,将老数组的数据复制到新数组,若不释放锁,导致在扩容过程中其他的线程无法进行读写,比较影响效率,所以先释放锁,通过CAS改变allocationSpinLock 的值来保证只有一个线程可以进行扩容
private void tryGrow(Object[] array, int oldCap) {
// 先释放锁
lock.unlock();
// 声明新数组
Object[] newArray = null;
// 如果allocationSpinLock 为0,说明当前没有线程在进行扩容
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {
try {
// 计算新数组的容量
int newCap = oldCap + ((oldCap < 64) ?
// 如果数组长度比较小,这里加快扩容长度速度。(还可以避免数组长度为0的时候,无效的扩容,正常是不会出现的,除非瞎玩,比如反射)
(oldCap + 2) : // grow faster if small
// 如果长度大于等于64了,每次扩容到1.5倍即可。
(oldCap >> 1));
// 如果新数组长度大于MAX_ARRAY_SIZE
if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
// 声明minCap,长度为老数组 + 1
int minCap = oldCap + 1;
// 老数组+1变为负数,或者老数组长度已经大于MAX_ARRAY_SIZE了,无法扩容了。
if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
throw new OutOfMemoryError();
// 如果没有超过限制,直接设置为最大长度即可
newCap = MAX_ARRAY_SIZE;
}
// 新数组长度,得大于老数组长度
// queue == array 此处的判断是为了避免并发扩容,比如有一个线程刚刚扩容完毕,
// 把newArray赋值给了queue,此时在外面进行while循环某个线程进来了,如果不判断
// queue == array是否相等,就又申请了一个新数组,相当于进行了两次扩容
if (newCap > oldCap && queue == array)
newArray = new Object[newCap];
} finally {
// 还原标记
allocationSpinLock = 0;
}
}
// 如果到了这,newArray依然为null,说明这个线程没有进到if方法中,去构建新数组
if (newArray == null) // back off if another thread is allocating
// 稍微等一手,让扩容的线程把扩容完成
Thread.yield();
// 加锁
lock.lock();
// 确认是当前线程申请的新数组,且没有并发问题,则将新数组赋值给queue,并将老数组的内容拷贝到新数组
if (newArray != null && queue == array) {
queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
添加进来的数据,需要保证二叉堆的平衡,也就是要保证按优先级出队,所以添加进来的数据可能是需要调整的,寻找正确的位置再放下
// k当前数组元素的个数,也就是x要放的位置,那就是说默认想把它放在队尾
// x是需要加入队列的数据
// array是数组
private static <T> void siftUpComparable(int k, T x, Object[] array) {
// 将插入的元素直接强转为Comparable 所以自定义数据类型,必须实现Comparable
// 这行强转,会导致添加没有实现Comparable的元素,直接报错。
Comparable<? super T> key = (Comparable<? super T>) x;
// k>0,说明队列中有数据,有数据才需要调整位置,没有数据,默认放在0位置就可以
while (k > 0) {
// 找到父节点 (k - 1) >>> 1 找父节点的公式
int parent = (k - 1) >>> 1;
// 拿到父节点的值
Object e = array[parent];
// 如果当前加入队列的元素比父节点大,则直接放
if (key.compareTo((T) e) >= 0)
// 跳出循环
break;
// 如果加入的元素比父节点小,则将父节点放到k位置,也就是父节点往下移动
array[k] = e;
// 重新设置x节点需要放置的位置。k就来到了父节点原本的位置,此时再进行while判断,
// 如果合适则放下,不合适继续重复刚才的操作,直到找到合适的位置,或者找到根节点
k = parent;
}
array[k] = key;
}
3.3 put(E e)
参考offer方法
public void put(E e) {
offer(e); // never need to block
}
3.4 offer(E e, long timeout, TimeUnit unit)
参考offer方法
public boolean offer(E e, long timeout, TimeUnit unit) {
return offer(e); // never need to block
}
4、读取操作
读取操作是存在线程挂起的情况的,因为如果数组中元素个数为0,当前线程如果执行了take方法,必然需要挂起。
其次获取数据,因为是优先级队列,所以需要从二叉堆栈顶拿数据,直接拿索引为0的数据即可,但是拿完之后,需要保持二叉堆结构。
4.1 poll()
出队,从队列头部取出元素之后,那么根节点就没有了,此时需要调整二叉堆,找出新的跟节点,保证二叉堆的结构平衡,且按优先级出队
public E poll() {
// 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 执行出队
return dequeue();
} finally {
// 释放锁
lock.unlock();
}
}
private E dequeue() {
// n是尾巴元素的下标
int n = size - 1;
// n小于0,说明队列中没有元素
if (n < 0)
return null;
else {
// 获取queue
Object[] array = queue;
// 拿到0位置的元素,就是要返回的元素
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);
// 元素个数减1
size = n;
// 返回result
return result;
}
}
// k是0,默认是下标0
// x是队列尾部的元素
// array是数组本身
// 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; // loop while a non-leaf
// 做了超过一半,就不需要再往下找了。
while (k < half) {
// 找左子节点索引,一个公式,可以找到当前节点的左子节点
int child = (k << 1) + 1; // assume left child is least
// 左子节点的值
Object c = array[child];
// 右子节点的下标
int right = child + 1;
// right < n 判断是否有右子节点
// 判断左节点是否大于右节点
if (right < n &&
((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
//如果左大于右,那么c就执行右
c = array[child = right];
// 比较最后一个节点是否小于当前的较小的子节点 如果是则跳出循环,末尾的元素直接放到跟节点
if (key.compareTo((T) c) <= 0)
break;
// 否则的话,跟节点则为较小的子节点
array[k] = c;
// k重置到之前的子节点位置,重新循环,重新给末尾元素寻找合适的位置
k = child;
}
// 上面while循环搞定后,可以确认整个二叉堆中,数据已经移动ok了,只差当前k的位置数据是null
// 将最后一个索引的数据放到k的位置
array[k] = key;
}
}
4.2 take()
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;
}
4.3 poll(long timeout, TimeUnit unit) throws InterruptedException
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;
}