本篇我们将分析阻塞队列PriorityBlockingQueue实现原理,该阻塞队列每次取出的都是最小的对象,可以满足一定的实际场景。
阻塞队列PriorityBlockingQueue从不阻塞写线程,而当队列元素为空时,会阻塞读线程的读取,当然也有非阻塞的方法(poll)。该阻塞队列适用于读多于写的场景,不然,写线程过多,会导致内存消耗过大,影响性能。
阻塞队列PriorityBlockingQueue原理
1.写线程不阻塞,读线程在队列为空时阻塞
当队列为满时,写线程不会阻塞,而会尝试去扩容,扩容成功就继续向阻塞队列写入数据。当队列为空时,读线程会阻塞等待,直到队列不为空,被写线程唤醒。因此该阻塞队列适用于读多于写的场景,不然,写线程过多,会导致内存消耗过大,影响性能。读写线程共用同一把独占锁。
2.堆存储结构
阻塞队列PriorityBlockingQueue每次取出的都是最小(或最大)的对象,而其底层的存储结构正是堆存储(ps:堆结构需要满足每一个父节点小于(或大于)每一个子节点),所以每次向阻塞队列添加的时候,需要根据元素大小,调整其在堆中的位置,注意不需要对所有元素排序,只对父子节点排序,只要满足堆性质即可。这样每次从阻塞队列中取出元素时,直接取出堆顶元素,即最小值(或最大值),然后重新调整堆。
3.扩容机制
当阻塞队列PriorityBlockingQueue满时,写线程会尝试扩容(扩增old+2或者old/2),注意,写线程扩容的时候会先释放独占锁,并获取一个扩容独占锁(此锁仅用于扩容,防止多个写线程同时扩容),这样做的目的是提高读线程的吞吐量。扩容的本质其实就是新建一个更大的object数组,然后把阻塞队列PriorityBlockingQueue中的原阻塞队列引用替换成新的数组。
4.PriorityBlockingQueue需要元素自然排序或者提供比较器
前面说过,阻塞队列PriorityBlockingQueue使用的是堆存储结构,那么就需要对父子节点进行排序,所以需要阻塞队列PriorityBlockingQueue存储的元素实现了Comparable接口,或者在新建阻塞队列PriorityBlockingQueue的时候提供比较器。
PriorityBlockingQueue源码分析
首先看一下,阻塞队列PriorityBlockingQueue的成员变量,分别代表什么含义。
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;
//比较器,元素没有实现comparable接口时,需提供比较器
private transient Comparator super E> comparator;
//独占锁,读写线程共用这一把锁
private final ReentrantLock lock;
//读线程等待队列,写线程永远不会阻塞
private final Condition notEmpty;
//写线程扩容锁,通过CAS控制,只有一个写线程会将此变量从0变成1
private transient volatile int allocationSpinLock;
阻塞队列PriorityBlockingQueue添加元素的方法:
public void put(E e) {
offer(e); // 从不需要阻塞。
}
public boolean offer(E e) {
//1.不支持null值
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
//2.加锁
lock.lock();
int n, cap;
Object[] array;
//3.如果队列满了,就调用tryGrow进行扩容
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap);
try {
Comparator super E> cmp = comparator;
//4.使用自然排序,将新元素添加到堆中。
if (cmp == null)
siftUpComparable(n, e, array);
//4.使用比较器,将新元素添加到堆中。
else
siftUpUsingComparator(n, e, array, cmp);
//5.队列中元素数量增1
size = n + 1;
//6.唤醒读线程
notEmpty.signal();
} finally {
//7.释放锁
lock.unlock();
}
return true;
}
从上面分析可知,当阻塞队列满了,写线程就会调用tryGrow方法对队列扩容,扩容成功后再向队列添加新元素。下面就看看扩容方法tryGrow:
private void tryGrow(Object[] array, int oldCap) {
//1.先释放锁,好让读线程继续读,提高吞吐量。
lock.unlock();
//2.新阻塞队列
Object[] newArray = null;
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {
//3.获取扩容锁,通过CAS将allocationSpinLock从0变成1,仅有一个写线程获取该锁进行扩容
try {
//4.扩容大小
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // grow faster if small
(oldCap >> 1));
//5.如果新容量大于MAX_ARRAY_SIZE,那么可能内存溢出
if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
int minCap = oldCap + 1;
//6.如果旧容量已经到达最大值,那么此次扩容失败
if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
throw new OutOfMemoryError();
//7.否则,就取最大容量MAX_ARRAY_SIZE
newCap = MAX_ARRAY_SIZE;
}
//8.如果新容量大于旧容量并且原阻塞队列没有变(这里考虑了当扩容写线程释放扩容锁后,又有一个写
//线程过来扩容,那么不允许再次扩容)
if (newCap > oldCap && queue == array)
newArray = new Object[newCap];
} finally {
//9.释放扩容锁
allocationSpinLock = 0;
}
}
//10.如果有写线程正在扩容,就让出CPU等待
if (newArray == null) // back off if another thread is allocating
Thread.yield();
//11.再次获取独占锁
lock.lock();
//12.再次判断是否是首个获取扩容所扩容的写线程,如果是就将阻塞队列引用为新队列。
if (newArray != null && queue == array) {
queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
添加元素到堆中的方法siftUpComparable,siftUpUsingComparator这两个一模一样,区别只是自然排序还是比较器排序。
//参考最上面阻塞队列原理2
private static 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;
}
从阻塞队列PriorityBlockingQueue取出元素的方法:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//1.可中断获取锁
lock.lockInterruptibly();
E result;
try {
//2.如果阻塞队列为空,就阻塞等待
while ( (result = dequeue()) == null)
notEmpty.await();
} finally {
//3.释放锁
lock.unlock();
}
return result;
}
下面看一下出队列的方法dequeue:
private E dequeue() {
int n = size - 1;
//1.如果阻塞队列为空,返回null
if (n < 0)
return null;
else {
//2.否则取出堆顶元素
Object[] array = queue;
E result = (E) array[0];
E x = (E) array[n];
array[n] = null;
Comparator super E> cmp = comparator;
//3.重新调整堆
if (cmp == null)
siftDownComparable(0, x, array, n);
else
siftDownUsingComparator(0, x, array, n, cmp);
size = n;
return result;
}
}
总结: 阻塞队列PriorityBlockingQueue从不阻塞写线程,当队列满时,写线程会尝试扩容阻塞队列,扩容成功后再向阻塞队列中新增元素,而当队列元素为空时,会阻塞读线程的读取,当然也有非阻塞的方法(poll)。该阻塞队列适用于读多于写的场景,不然,写线程过多,会导致内存消耗过大,影响性能。阻塞队列采用堆存储结构,因此每次冲阻塞队列取出的元素总是最小元素(或最大元素)。而堆存储需要提供比较器或者元素实现了阻塞接口,否则程序会抛出ClassCastException。