PriorityBlockingQueue 是 Java 并发包 (java.util.concurrent) 中的一个线程安全的优先级队列实现。它结合了 PriorityQueue 的优先级排序特性和 BlockingQueue 的线程安全阻塞特性。
目录
基本特性
-
无界队列:理论上可以无限增长(受限于内存),但可以设置初始容量
-
线程安全:内部使用 ReentrantLock 保证线程安全
-
阻塞操作:当队列为空时,获取操作会阻塞;由于是无界队列,插入操作不会阻塞
-
优先级排序:元素按照自然顺序或 Comparator 指定的顺序出队
底层数据结构
基于二叉堆的数组实现
PriorityBlockingQueue 内部使用一个平衡的二叉堆(通常是最小堆)来实现优先级队列,使用数组作为底层存储:
private transient Object[] queue; // 存储元素的数组
-
堆结构保证第一个元素总是优先级最高(最小)的元素
-
对于位置
n的元素:-
左子节点在
2*n+1 -
右子节点在
2*n+2 -
父节点在
(n-1)/2
-
动态扩容机制
由于是无界队列,当数组容量不足时会自动扩容:
-
初始容量默认为 11 (
DEFAULT_INITIAL_CAPACITY) -
当队列满时自动扩容,每次扩容约 50%(
newCapacity = oldCapacity + (oldCapacity < 64 ? oldCapacity + 2 : oldCapacity >> 1)) -
最大容量为
Integer.MAX_VALUE - 8(数组头保留空间) -
扩容时释放主锁,避免阻塞其他操作
-
使用 CAS 操作控制扩容过程
-
小容量时加倍增长,大容量时增长50%
private void tryGrow(Object[] array, int oldCap) {
lock.unlock(); // 扩容前先释放锁,允许其他操作进行
Object[] newArray = null;
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset, 0, 1)) {
try {
// 计算新容量:小于64时加倍,否则增长50%
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) :
(oldCap >> 1));
if (newCap - MAX_ARRAY_SIZE > 0) { // 处理溢出
int minCap = oldCap + 1;
if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
throw new OutOfMemoryError();
newCap = MAX_ARRAY_SIZE;
}
newArray = new Object[newCap];
} finally {
allocationSpinLock = 0;
}
}
// 其他线程可能也在扩容,让出CPU
if (newArray == null)
Thread.yield();
lock.lock(); // 重新获取锁
if (newArray != null && queue == array) {
queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
并发控制机制
1. 主要锁结构
private final ReentrantLock lock; // 主锁,控制所有访问
private final Condition notEmpty; // 用于take操作的等待条件
-
所有修改操作(put/take等)都需要获取这个锁
-
读操作(如peek)也需要获取锁保证可见性
2. 自旋锁辅助扩容
private transient volatile int allocationSpinLock; // 扩容自旋锁
使用轻量级的自旋锁(通过CAS操作)控制扩容过程,避免在扩容期间完全阻塞其他操作。
阻塞的核心实现机制
1. 锁和条件变量
private final ReentrantLock lock; // 主锁
private final Condition notEmpty; // 取元素时的等待条件
// 注意:没有notFull条件,因为是无界队列
2. 阻塞发生的两种情况
(1) 取元素时阻塞(队列为空)
当消费者线程调用 take() 方法时,如果队列为空,线程会被阻塞:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 可中断的获取锁
try {
E result;
// 循环检查直到获取到元素
while ((result = dequeue()) == null) {
notEmpty.await(); // 释放锁并等待
}
return result;
} finally {
lock.unlock();
}
}
阻塞过程:
-
线程获取锁
-
检查队列是否为空(
dequeue()返回null) -
如果为空,调用
notEmpty.await():-
原子性地释放锁
-
线程进入等待状态
-
被唤醒后会重新获取锁
-
-
循环检查直到成功取出元素
(2) 等待锁时阻塞
多个线程同时操作队列时,未获取到锁的线程会在锁层面阻塞:
lock.lockInterruptibly(); // 这里可能阻塞
唤醒机制
public boolean offer(E e) {
// ...省略其他代码...
notEmpty.signal(); // 唤醒一个等待的消费者线程
return true;
}
唤醒过程:
-
生产者线程获取锁并成功插入元素
-
调用
notEmpty.signal()唤醒一个等待的消费者线程 -
被唤醒的消费者线程会尝试重新获取锁
-
获取锁成功后继续执行
take()中的循环检查
为什么没有 put 阻塞?
因为 PriorityBlockingQueue 是无界队列,所以:
-
put()方法实际上直接调用了offer() -
不需要
notFull条件变量 -
插入操作永远不会阻塞(除非达到 Integer.MAX_VALUE)
中断处理
PriorityBlockingQueue 支持响应中断:
lock.lockInterruptibly(); // 可被中断的锁获取
notEmpty.await(); // 可被中断的条件等待
当线程在阻塞状态被中断时:
-
会抛出
InterruptedException -
线程的中断状态会被清除
-
锁会被正确释放
与 ArrayBlockingQueue 的阻塞对比
| 特性 | PriorityBlockingQueue | ArrayBlockingQueue |
|---|---|---|
| 队列边界 | 无界 | 有界 |
| put() 是否阻塞 | 永不阻塞 | 队列满时阻塞 |
| take() 是否阻塞 | 队列空时阻塞 | 队列空时阻塞 |
| 使用的条件变量 | 只有 notEmpty | notEmpty 和 notFull |
| 锁数量 | 单个锁 | 单个锁 |
实际阻塞示例
PriorityBlockingQueue<Integer> queue = new PriorityBlockingQueue<>();
// 消费者线程
new Thread(() -> {
try {
System.out.println("尝试取元素...");
Integer item = queue.take(); // 这里会阻塞
System.out.println("获取到: " + item);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 模拟3秒后生产者添加元素
Thread.sleep(3000);
queue.put(42); // 唤醒消费者
输出
尝试取元素...
(3秒后)
获取到: 42
总结
PriorityBlockingQueue 的阻塞特性:
-
单向阻塞:只在取元素时(队列为空)阻塞
-
条件等待:通过
notEmpty.await()实现优雅等待 -
自动唤醒:插入元素时自动唤醒等待的消费者
-
线程安全:所有操作由同一把锁保护
-
无界特性:插入操作永不阻塞(可能引发OOM需要注意)
这种设计使其非常适合需要按优先级处理任务的生产者-消费者场景,同时避免了有界队列的复杂性。
HashMap详解 HashLink详解 ArrayList详解 CopyOnWriteArrayList详解 HashSet详解 LinkedHashMap详解 LinkedHashSet详解 TreeSet详解 TreeMap详解 ConcurrentHashMap详解 PriorityBlockingQueue 详解 PriorityQueue 详解
3806

被折叠的 条评论
为什么被折叠?



