PriorityBlockingQueue源码解析
一、概述
PriorityBlockingQueue
是一个支持优先级的无界阻塞队列。- 队列内部使用 数组 存储数据。
- 由于数组初始化时,长度已经确定,而PriorityBlockingQueue又是一个无界队列,因此内部存在自动扩容机制 (类似ArrayList)。
PriorityBlockingQueue
使用最小堆来实现。
本文涉及的知识点:
- 数组实现的相关数据结构:ArrayList 源码分析
- 队列的相关知识:Queue 综述
- 阻塞队列的概念:并发容器(一) — 综述
- ArrayBlockingQueue源码解析
- 阻塞功能的实现涉及Condition接口:Condition接口
- 并发安全涉及可重入锁ReentrientLock:ReentrantLock源码解析
- 二叉堆 - 最小堆
二、源码解析
普通队列包含入队和出队两种操作,阻塞队列在普通队列原有的基础上实现了阻塞功能。
阻塞队列提供了4套API 来操作队列,本文我们只分析阻塞相关的操作 (入队/出队)。其他操作可以参考:ArrayBlockingQueue源码解析。
下面将从以下几个方面进行分析:
- 构造函数:
PriorityBlockingQueue()
- 最小堆的上浮、下沉操作:
siftUpUsingComparator()
、siftDownUsingComparator()
- 队列具有阻塞功能的入队、出队操作:
put(e)
、take()
- 队列的扩容操作:
tryGrow()
1. 构造函数
PriorityBlockingQueue 中只维护了一个Condition,ArrayBlockingQueue 中维护了两个了Condition。
// PriorityBlockingQueue.class
final ReentrantLock lock; //可重入锁,在添加、移除元素时加锁保证安全性
private final Condition notEmpty; //锁的非空条件
private transient Comparator<? super E> comparator; //队列中元素跟新插入元素的比较器
final Object[] queue; //实际添加元素的容器
private transient int size; //队列中元素的个数
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];
}
2. 最小堆的上浮、下沉操作
最小堆的上浮、下沉操作:siftUpUsingComparator()
、siftDownUsingComparator()
siftUpUsingComparator()
/*
* 最小堆:上浮操作
* @param k 新添加的元素将要被插入的位置
* @param x 新添加的元素
* @param 存放数据的数组容器
*/
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; //将插入的元素添加到最小堆的合理位置。
}
/**
* 最小堆:下沉操作
* @param k the position to fill
* @param x the item to insert
* @param array the heap array
* @param n heap size 堆大小
*/
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;
}
}
3. 具有阻塞功能的入队、出队操作
队列具有阻塞功能的入队、出队操作:put(e)
、take()
public void put(E e) {
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; //n表示队列中插入的元素个数,cap表示当前数组容器的大小。
Object[] array;
// 当前个数>=数组容器长度时,需要进行扩容操作,这个在第4部分分析
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap); //数组扩容
try {
Comparator<? super E> cmp = comparator;
if (cmp == null)
// 请参考第2部分,新插入的数据进行上浮操作
siftUpComparable(n, e, array);
else
// 请参考第2部分,新插入的数据进行上浮操作
siftUpUsingComparator(n, e, array, cmp);
size = n + 1; //队列元素个数增加1
notEmpty.signal(); //通知获取数据时被阻塞的线程可以获取数据了。
} finally {
lock.unlock(); //释放锁
}
return true;
}
// *********************
// 出队列
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); //加锁,可响应中断
E result;
try {
while ( (result = dequeue()) == null) //dequeue()==null表示当前队列为空
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(0, x, array, n);
else
// 对数据进行下沉操作
siftDownUsingComparator(0, x, array, n, cmp);
size = n;
return result;
}
}
4. 队列的扩容操作
队列的扩容操作:tryGrow()
private void tryGrow(Object[] array, int oldCap) {
// 释放锁(当前线程释放锁后,其他线程可以获取元素,使容量下降,其他插入元素的线程也可以不扩容直接向数组中添加元素,提高效率。)
lock.unlock(); // must release and then re-acquire main lock
Object[] newArray = null;
// 自旋锁,CAS操作
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {
try {
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // grow faster if small
(oldCap >> 1)); //数组容量超过64个时,每次扩容50%
if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
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;
}
}
// newArray == null 表示有线程正在扩容,所以当前线程要让出自己的CPU。
if (newArray == null) // back off if another thread is allocating
Thread.yield();
lock.lock(); // 数据拷贝的过程需要保证线程安全
if (newArray != null && queue == array) {
queue = newArray;
// 进行数据拷贝
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
三、小结
PriorityBlockingQueue
是一个数组实现的具有优先级的无界阻塞列。- 只有
put(e)
、take()
才能实现阻塞功能,其他方法都不具备阻塞功能。 PriorityBlockingQueue
是通过最小堆来实现优先级的,队列头数据是最小的。