1. 概述
PriorityBlockingQueue 是 Java 并发包(java.util.concurrent)中提供的一个无界阻塞队列,它支持按照元素的优先级进行排序。与普通队列的 FIFO 规则不同,PriorityBlockingQueue 中的元素按照其自然顺序或者通过构造时提供的 Comparator 进行排序,队列的头部始终是当前优先级最高的元素(最小堆实现,即值最小的元素在堆顶,等价于最高优先级)。
核心特点:
- 无界队列:理论上队列容量无限,插入元素(
put、offer)永远不会阻塞。但内部基于数组存储,当元素数量超过当前数组长度时会触发动态扩容。 - 优先级排序:依赖二叉堆(默认最小堆)实现,元素必须可比较,要么实现
Comparable接口,要么在构造时传入Comparator。 - 不支持
null元素:与大多数阻塞队列一致,null被用作特殊标记(如poll()返回null表示队列为空),因此禁止插入null。 - 阻塞行为不对称:
take()在队列为空时阻塞等待元素;put()/offer()永不阻塞。 - 内部数据结构:基于数组
Object[] queue的二叉堆,搭配一把ReentrantLock和唯一的条件队列notEmpty(无notFull)。
典型应用场景:
- 优先级任务调度(例如紧急程度高的任务先执行)。
- 带权重的生产者-消费者模型(例如消息按重要性处理)。
- 需要全局排序但无需容量限制的缓冲区。
与 ArrayBlockingQueue / LinkedBlockingQueue 的主要区别
| 特性 | PriorityBlockingQueue | ArrayBlockingQueue | LinkedBlockingQueue |
|---|---|---|---|
| 有界性 | 无界(动态扩容) | 有界(构造时固定容量) | 可选有界(默认 Integer.MAX_VALUE) |
| 排序特性 | 按优先级排序(最小堆) | FIFO | FIFO |
| 锁结构 | 单锁 ReentrantLock + 1 个条件 notEmpty | 单锁 ReentrantLock + 2 个条件 | 双锁(putLock + takeLock),两个条件 |
| 扩容机制 | 动态数组扩容(tryGrow 使用 CAS 并发扩容) | 固定容量,不可扩容 | 链表节点动态创建,无扩容概念 |
| 阻塞生产(put) | 否(因无界) | 是(队列满时阻塞) | 是(若指定容量且满时阻塞) |
| 阻塞消费(take) | 是(队列空时阻塞) | 是 | 是 |
2. 核心方法说明
| 方法 | 参数 | 返回值 | 阻塞行为 | 异常 |
|---|---|---|---|---|
PriorityBlockingQueue() | 无 | 构造器,初始容量 11,默认自然排序 | 无 | 无 |
PriorityBlockingQueue(int initialCapacity) | initialCapacity:初始容量(必须 >0) | 构造器 | 无 | IllegalArgumentException |
PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator) | 初始容量、比较器 | 构造器 | 无 | IllegalArgumentException |
PriorityBlockingQueue(Collection<? extends E> c) | 初始集合 | 构造器,根据集合初始化堆 | 无 | NullPointerException 如果集合或元素为 null |
put(E e) | e:元素 | void | 不阻塞(无界队列),直接插入,若容量不足则扩容 | NullPointerException、ClassCastException(如果元素不可比较) |
offer(E e) | e:元素 | boolean:总是返回 true(无界) | 不阻塞 | NullPointerException、ClassCastException |
offer(E e, long timeout, TimeUnit unit) | 元素、超时时间、时间单位 | boolean:总是返回 true(超时参数被忽略) | 不阻塞 | NullPointerException、ClassCastException |
take() | 无 | E:队首元素(优先级最高) | 如果队列空,线程阻塞直到有元素 | InterruptedException |
poll() | 无 | E:队首元素,空返回 null | 不阻塞 | 无 |
poll(long timeout, TimeUnit unit) | 超时时间、时间单位 | E:元素,超时后仍空返回 null | 等待指定时间 | InterruptedException |
peek() | 无 | E:队首元素(不移除),空返回 null | 不阻塞 | 无 |
size() | 无 | int:当前元素个数 | 无 | 无 |
remainingCapacity() | 无 | int:总是返回 Integer.MAX_VALUE(因为无界) | 无 | 无 |
drainTo(Collection<? super E> c) | 目标集合 | int:转移的元素数量 | 无 | NullPointerException |
drainTo(Collection<? super E> c, int maxElements) | 目标集合、最大转移数 | int:实际转移数 | 无 | NullPointerException |
3. 核心原理与源码分析(基于 JDK 8)
3.1 数据结构
PriorityBlockingQueue 的核心字段如下:
// 二叉堆数组,存储队列元素,索引0为堆顶
private transient Object[] queue;
// 当前队列元素个数
private transient int size;
// 比较器,为null时使用元素自然顺序(Comparable)
private transient Comparator<? super E> comparator;
// 全局互斥锁,所有对队列的修改操作均需持有此锁
private final ReentrantLock lock;
// 条件队列,用于take()时队列为空的阻塞等待
private final Condition notEmpty;
// 扩容时的自旋锁,通过CAS控制只有一个线程进行数组扩容
private transient volatile int allocationSpinLock;
// 仅用于序列化兼容,实际不使用
private PriorityQueue<E> q;
为什么只需要 notEmpty 而无需 notFull?
因为队列是无界的,生产者插入元素永远不会因为队列满而阻塞。因此条件等待只发生在消费者侧(队列空时等待),不需要生产者等待条件。
3.2 二叉堆原理
PriorityBlockingQueue 使用最小堆(元素越小优先级越高)的数组表示。对于数组索引 i:
- 左子节点索引:
2 * i + 1 - 右子节点索引:
2 * i + 2 - 父节点索引:
(i - 1) / 2
堆的调整操作:
- 上浮(
siftUp):插入元素时,先将元素放在数组末尾,然后与父节点比较,若比父节点小则交换,直到满足堆性质。 - 下沉(
siftDown):移除堆顶后,将数组最后一个元素移到堆顶,然后与较小的子节点比较,若大于子节点则交换,直至满足堆性质。
3.3 构造器
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];
}
从集合构造时,会调用 heapify() 方法将整个数组调整为一个堆(自下而上的 siftDown),时间复杂度 O(n)。
3.4 插入元素(put / offer)
put 和带超时的 offer 均直接调用 offer(E 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 {
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftUpComparable(n, e, array);
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
notEmpty.signal(); // 唤醒等待的消费者
} finally {
lock.unlock();
}
return true;
}
流程解析:
- 加锁(
lock.lock())。 - 检查元素非
null。 - 若
size >= queue.length,调用tryGrow进行扩容(注意:扩容过程可能释放锁)。 - 执行
siftUp将元素插入堆中合适位置。 size递增,并调用notEmpty.signal()唤醒可能阻塞在take的线程(仅当原来队列为空时唤醒有效,但此处无条件唤醒也无妨)。- 解锁。
3.5 扩容机制(tryGrow)
这是 PriorityBlockingQueue 设计中的精妙之处:扩容时释放主锁,以减少锁竞争。
private void tryGrow(Object[] array, int oldCap) {
lock.unlock(); // 释放锁,允许其他线程并发操作
Object[] newArray = null;
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset, 0, 1)) {
try {
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // 小容量增长快
(oldCap >> 1)); // 大容量增长50%
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 {
allocationSpinLock = 0;
}
}
if (newArray == null) // 其他线程正在扩容,让出CPU
Thread.yield();
lock.lock(); // 重新获取锁
if (newArray != null && queue == array) {
queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
设计要点:
- 扩容前主动释放锁,使其他生产者/消费者能够继续操作队列(例如消费者可以出队,从而减少实际元素数量,降低扩容必要性)。
- 使用 CAS 操作
allocationSpinLock,确保同一时刻只有一个线程进行数组分配和复制,避免多个线程同时扩容造成资源浪费。 - 扩容完成后重新获取锁,将旧数组内容拷贝到新数组,并更新
queue引用。 - 如果 CAS 失败,说明已有线程在扩容,当前线程主动
yield(),等待其他线程完成扩容。
3.6 取出元素(take / poll)
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;
}
dequeue() 方法负责从堆中移除堆顶并维持堆序:
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;
}
}
流程:
- 若队列为空(
size == 0),返回null,take会在循环中调用notEmpty.await()阻塞。 - 取出堆顶元素
result = queue[0]。 - 将数组最后一个元素
x = queue[n]取出并置null。 - 调用
siftDown(0, x)将x下沉到合适位置,以维持最小堆性质。 - 更新
size,返回结果。
poll() 不阻塞,若队列空直接返回 null;超时版本使用 awaitNanos()。
3.7 比较器支持
siftUp 和 siftDown 均有两个版本:Comparable 版本和 Comparator 版本。以 siftUpComparable 为例:
private static <T> 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;
}
若提供了 Comparator,则使用 comparator.compare(x, e) 进行比较。
3.8 无界队列的阻塞特性
PriorityBlockingQueue 的阻塞模型是非对称的:
- 生产者永远不阻塞:因为队列无容量上限,
put总能成功插入(可能会触发扩容,但最终会成功)。 - 消费者可能阻塞:当队列为空时,
take会等待,直到有元素被插入并唤醒。
这种设计使得 PriorityBlockingQueue 非常适合生产速度远大于消费速度的场景,但需要额外关注内存占用,防止队列无限增长。
3.9 锁与条件
PriorityBlockingQueue 只使用一把 ReentrantLock,这与 ArrayBlockingQueue 类似,但只有一个条件 notEmpty。所有入队、出队、扩容操作都受同一把锁保护,保证线程安全。然而,扩容时的锁释放是它与 ArrayBlockingQueue 的重要区别,提高了并发下的伸缩性。
3.10 序列化
序列化通过 writeObject 和 readObject 方法实现。序列化时会将内部数组的元素转存到默认序列化机制中;反序列化时重新构建堆结构(调用 heapify),恢复优先级队列状态。
非常感谢您的提醒。我在上文中已为每个 Mermaid 图提供了配套描述,但为了严格满足“每个图附详细文字描述”的要求,现对每个图补充更详尽的解析,涵盖图中元素含义、执行路径、与源码的对应关系以及关键设计要点。
关键流程
4.1 类图详细说明
classDiagram
class PriorityBlockingQueue~E~ {
-Object[] queue
-int size
-Comparator~E~ comparator
-ReentrantLock lock
-Condition notEmpty
-volatile int allocationSpinLock
+PriorityBlockingQueue()
+PriorityBlockingQueue(int, Comparator)
+put(E e)
+offer(E e) boolean
+take() E
+poll() E
+drainTo(Collection) int
-tryGrow(Object[], int)
-siftUp(int, E)
-siftDown(int, E)
}
详细说明:
Object[] queue:二叉堆的底层数组,索引 0 存放优先级最高(最小堆中值最小)的元素。数组长度动态变化,由扩容机制决定。int size:当前队列中元素的数量,所有修改操作都会更新该值,它决定堆的边界以及是否触发扩容。Comparator<? super E> comparator:比较器,若为null则依赖元素的自然顺序(Comparable)。该字段决定堆调整时的比较逻辑分支。ReentrantLock lock:全局互斥锁,所有入队、出队、扩容的公共逻辑均需持有该锁,保证线程安全。Condition notEmpty:条件队列,由lock.newCondition()创建,用于在take()时队列空则阻塞线程,并在元素插入后唤醒等待者。volatile int allocationSpinLock:扩容专用 CAS 标志位(0 表示未锁定,1 表示已被某线程占用),确保仅有一个线程执行数组分配和复制,从而避免并发扩容冲突。- 主要方法:
put/offer:入队操作,因无界故永不阻塞,put内部调用offer。take/poll:出队操作,take空则阻塞,poll空则返回null。drainTo:批量转移元素至指定集合。tryGrow:扩容核心方法,涉及锁释放与 CAS 竞争。siftUp/siftDown:堆调整算法,维持最小堆性质。
4.2 二叉堆结构图详细说明
数组表示: | 索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | |------|---|---|---|---|---|---|---|---| | 值 | 1 | 3 | 2 | 7 | 6 | 5 | 4 |
逻辑树结构:
graph TD
1 --> 3
1 --> 2
3 --> 7
3 --> 6
2 --> 5
2 --> 4
详细说明:
- 该图展示了一个包含 7 个元素的最小堆。堆顶(索引 0) 的值为 1,是当前队列中优先级最高的元素(数字越小优先级越高)。
- 父子关系计算公式:
- 对于索引
i的节点,其左子节点索引为2*i + 1,右子节点索引为2*i + 2。 - 任意节点
i的父节点索引为(i - 1) / 2(整数除法)。
- 对于索引
- 堆性质验证:
- 节点 1(索引 0)的两个子节点分别为 3(索引 1)和 2(索引 2),均大于等于 1。
- 节点 3(索引 1)的子节点 7(索引 3)和 6(索引 4)均大于等于 3,依此类推。
- 与源码映射:
PriorityBlockingQueue的内部数组queue正是按照此规律存储。插入新元素时,会先将元素放在数组末尾(size位置),然后通过siftUp上浮至合适位置;移除堆顶时,将最后一个元素移到堆顶,再通过siftDown下沉恢复堆序。
4.3 插入元素(offer)流程图详细说明
graph TD
Start([开始 offer]) --> Lock[加锁 lock.lock]
Lock --> CheckNull{ e == null? }
CheckNull -- 是 --> NPE[抛出 NullPointerException]
CheckNull -- 否 --> CheckCap[判断 size >= queue.length]
CheckCap -- 是 --> TryGrow[调用 tryGrow 扩容]
TryGrow --> Unlock1[释放锁, CAS竞争]
Unlock1 --> Copy[分配新数组并复制]
Copy --> Relock[重新获取锁]
Relock --> SiftUp
CheckCap -- 否 --> SiftUp[执行 siftUp 上浮调整]
SiftUp --> IncSize[size++]
IncSize --> Signal{原 size == 0?}
Signal -- 是 --> Notify[notEmpty.signal]
Signal -- 否 --> Unlock2
Notify --> Unlock2[lock.unlock]
Unlock2 --> ReturnTrue[返回 true]
详细说明:
- 加锁:
offer方法开始即调用lock.lock(),确保后续对size、queue的访问是线程安全的。 - 空值检查:若
e == null,直接抛出NullPointerException,因为队列不允许null元素。 - 容量检查与扩容:
- 判断当前元素数量
size是否大于等于数组长度queue.length。 - 若是,则调用
tryGrow进行扩容。关键设计:tryGrow内部会先释放锁,然后通过 CAS 竞争allocationSpinLock来获得扩容权限。成功获得权限的线程分配新数组(新容量计算策略:若旧容量小于 64,增长oldCap + 2;否则增长oldCap >> 1即 50%),再将旧数组元素拷贝到新数组,最后重新获取锁,更新queue引用。此过程允许其他线程在扩容期间继续入队/出队(前提是不需要扩容),提高了并发性能。
- 判断当前元素数量
- 堆上浮调整:扩容完成后(或无需扩容时),根据是否存在
comparator,分别调用siftUpComparable或siftUpUsingComparator,将新元素从数组末尾(size索引位置)上浮至正确位置,维持最小堆性质。 - 更新状态与唤醒:
size自增。若插入前队列为空(size == 0),则通过notEmpty.signal()唤醒一个正在等待take的消费者线程。 - 解锁并返回:释放锁,由于是无界队列,永远返回
true。
4.4 取出元素(take)流程图详细说明
graph TD
Start([开始 take]) --> LockInt[lock.lockInterruptibly]
LockInt --> Deq[dequeue 尝试取堆顶]
Deq --> IsNull{ result == null? }
IsNull -- 是 --> Await[notEmpty.await 阻塞等待]
Await --> Deq
IsNull -- 否 --> SiftDown[执行 siftDown 下沉调整]
SiftDown --> UpdateSize[size--]
UpdateSize --> Unlock[lock.unlock]
Unlock --> Return[返回堆顶元素]
详细说明:
- 可中断加锁:
take()使用lock.lockInterruptibly()获取锁,响应线程中断。 - 循环尝试出队:
- 调用内部方法
dequeue()尝试从堆顶移除元素。dequeue()的逻辑为:若size == 0则返回null;否则取出queue[0],将数组最后一个元素x = queue[--size]置null,并调用siftDown(0, x)将x从堆顶下沉到合适位置,恢复堆序,最后返回原堆顶元素。 - 如果
dequeue()返回null,说明队列为空,则当前线程调用notEmpty.await()进入条件等待队列,释放锁并阻塞。当其他线程插入元素并调用notEmpty.signal()时,该线程被唤醒,重新竞争锁,然后再次循环尝试dequeue。
- 调用内部方法
- 更新 size 与解锁:
dequeue内部已执行siftDown并更新了size(size = n)。方法最后释放锁,返回取出的元素。
对于 poll() 非阻塞版本的说明:
poll()流程与take()类似,但若dequeue()返回null,则不进入等待,直接返回null,并释放锁。poll(long timeout, TimeUnit unit)则在dequeue()返回null时调用notEmpty.awaitNanos()进行限时等待,超时后若仍为null则返回null。
4.5 扩容机制(tryGrow)时序图详细说明
sequenceDiagram
participant ThreadA
participant ThreadB
participant Queue
participant Lock
participant CAS
ThreadA->>Lock: 已持有锁,发现需要扩容
ThreadA->>Lock: unlock 释放锁
ThreadA->>CAS: CAS(allocationSpinLock, 0, 1) 成功
ThreadA->>Queue: 分配新数组 newArray
ThreadA->>CAS: allocationSpinLock = 0
ThreadA->>Lock: lock 重新获取锁
ThreadA->>Queue: 复制旧数组到新数组,更新 queue
par 线程B尝试插入
ThreadB->>Lock: 竞争锁(可能在 ThreadA 释放锁期间获得锁)
ThreadB->>Queue: 发现仍需扩容,尝试 tryGrow
ThreadB->>CAS: CAS 失败(allocationSpinLock != 0)
ThreadB->>ThreadB: Thread.yield 让出CPU
ThreadB->>Lock: 等待锁释放后重入
end
详细说明:
- 场景设定:线程 A 在执行
offer时发现size >= queue.length,需要扩容。此时线程 A 已持有锁。 - 步骤分解:
- 释放锁:线程 A 主动调用
lock.unlock(),释放全局锁,使得其他线程(如线程 B)可以继续操作队列(例如执行take减小size,或执行无需扩容的offer)。这是提高并发度的关键优化。 - CAS 竞争扩容权限:线程 A 尝试用 CAS 将
allocationSpinLock从 0 修改为 1。若成功,表明当前没有其他线程正在进行扩容,线程 A 获得扩容执行权;若失败,则说明已有线程在扩容,线程 A 会执行Thread.yield()让出 CPU 并稍后重新尝试获取锁(代码中会再次循环判断)。 - 分配新数组:获得权限的线程 A 计算新容量(策略如前所述),并创建新的
Object[] newArray。完成后将allocationSpinLock重置为 0。 - 重新加锁并复制:线程 A 调用
lock.lock()重新获取锁(此时可能与其他线程竞争)。成功获取后,检查queue是否仍为旧数组(防止其他线程已完成扩容),若是,则将旧数组内容复制到新数组,并将queue引用指向新数组。 - 并发线程 B 的行为:在线程 A 释放锁期间,线程 B 可能获得锁并尝试插入,同样发现需要扩容,于是进入
tryGrow。由于allocationSpinLock已被线程 A 占据,线程 B 的 CAS 失败,它将执行Thread.yield(),然后重新竞争锁,等待线程 A 完成扩容后继续执行。
- 释放锁:线程 A 主动调用
设计价值:通过释放锁进行扩容,避免在数组复制(可能耗时较长)期间阻塞所有其他队列操作,显著提升了高并发下的响应性和吞吐量。
感谢您的反馈,针对 4.6 和 4.7 无法渲染的问题,我对 Mermaid 代码进行了重构和优化,将 4.6 拆分为两个独立的图(上浮与下沉),并修正了可能导致解析异常的语法。以下是修正后的完整内容。
4.6 堆调整操作流程图
4.6.1 上浮(siftUp)操作详细流程图
graph TD
Start([开始 siftUp]) --> CmpCheck{是否提供 Comparator}
CmpCheck -- 是 --> UseComp[使用 Comparator 比较]
CmpCheck -- 否 --> UseNat[使用 Comparable 比较]
UseComp --> LoopStart{k 大于 0}
UseNat --> LoopStart
LoopStart -- 否 --> Place[将 e 放入 array k]
LoopStart -- 是 --> CalcParent[计算 parent 等于 k 减 1 除以 2]
CalcParent --> Compare{ e 小于 parent 元素 }
Compare -- 是 --> MoveDown[将 parent 移动到 array k]
MoveDown --> UpdateK[更新 k 为 parent]
UpdateK --> LoopStart
Compare -- 否 --> Place
Place --> End([结束])
详细文字说明:
- 功能目标:当新元素
e插入到二叉堆的末尾(索引k)时,通过不断与父节点比较并交换,使新元素“上浮”到正确位置,维持最小堆性质(父节点 ≤ 子节点)。 - 比较器分支:
- 若构造队列时提供了
Comparator,则后续所有比较均调用comparator.compare(e, parent)。 - 若未提供,则强制将
e转换为Comparable并调用e.compareTo(parent)。若元素未实现Comparable,此处将抛出ClassCastException。
- 若构造队列时提供了
- 循环逻辑:
- 循环条件
k > 0:只要尚未到达堆顶(索引0),就继续比较。 - 计算父节点索引:
parent = (k - 1) >>> 1(无符号右移等效除以2)。 - 比较
e与父节点元素:若e小于父节点,则违反最小堆规则,需将父节点元素复制到当前位置array[k],并将k更新为父节点索引,继续向上比较。 - 若
e大于等于父节点,则当前位置即为正确插入点,跳出循环。
- 循环条件
- 最终放置:将元素
e放入array[k]。该操作的时间复杂度为 O(log n),因为最多比较树的高度次。 - 源码对应:此图精确对应
siftUpComparable和siftUpUsingComparator方法的执行逻辑。
4.6.2 下沉(siftDown)操作详细流程图
graph TD
Start([开始 siftDown]) --> CalcHalf[计算 half 等于 size 除以 2]
CalcHalf --> LoopCond{k 小于 half}
LoopCond -- 否 --> PlaceEnd[将 x 放入 array k]
PlaceEnd --> End([结束])
LoopCond -- 是 --> SetChild[child 等于 2乘k加1]
SetChild --> CmpBranch{是否提供 Comparator}
CmpBranch -- 是 --> CompChild[用 Comparator 比较左右子节点]
CmpBranch -- 否 --> NatChild[用 Comparable 比较左右子节点]
CompChild --> SelectSmall[child 指向较小的子节点]
NatChild --> SelectSmall
SelectSmall --> CompareX{ x 小于等于 array child }
CompareX -- 是 --> PlaceEnd
CompareX -- 否 --> MoveUp[将 array child 移动到 array k]
MoveUp --> UpdateK2[更新 k 为 child]
UpdateK2 --> LoopCond
详细文字说明:
- 功能目标:在移除堆顶元素后,将数组最后一个元素
x移动到索引k(通常为0),然后通过与子节点比较并交换,使该元素“下沉”到合适位置,重建最小堆。 - 边界计算:
half = size >>> 1表示第一个叶子节点的索引。当k >= half时,说明k位置没有子节点(或已到达叶子层),无需再比较,直接将x放入该位置。
- 选择较小子节点:
- 计算左子节点索引
child = 2*k + 1,右子节点索引right = child + 1。 - 根据是否存在
Comparator,比较左右子节点的值。如果right < size(右子节点存在)且右子节点值小于左子节点值,则将child指向右子节点。这一步确保child是较小的子节点。
- 计算左子节点索引
- 比较与交换:
- 将待下沉元素
x与较小的子节点array[child]比较。 - 若
x小于等于array[child],说明当前位置已满足堆性质,跳出循环。 - 否则,将较小的子节点上浮到当前位置
array[k] = array[child],并将k更新为child索引,继续向下比较。
- 将待下沉元素
- 最终放置:循环结束后将
x放入array[k]。时间复杂度同为 O(log n)。 - 源码对应:此图精确对应
siftDownComparable和siftDownUsingComparator方法。
4.7 drainTo 批量消费流程图
graph TD
Start([开始 drainTo]) --> Lock[lock 加锁]
Lock --> Init[初始化 n 等于 0]
Init --> LoopCheck{n 小于 maxElements 且 size 大于 0}
LoopCheck -- 否 --> Unlock[lock 解锁]
Unlock --> ReturnN[返回 n]
LoopCheck -- 是 --> Dequeue[调用 dequeue 取出堆顶元素]
Dequeue --> AddToColl[将元素添加到集合 c]
AddToColl --> IncN[n 自增 1]
IncN --> LoopCheck
详细文字说明:
- 原子操作保证:
drainTo从加锁开始到解锁结束,整个批量转移过程全程持有锁。这确保了:- 队列在转移过程中不会被其他线程修改。
- 转移出的元素严格遵循出队顺序,即优先级从高到低(最小堆堆顶优先)。
- 循环控制:
- 条件
n < maxElements:限制转移数量不超过指定上限。 - 条件
size > 0:当队列中没有元素时立即终止,即使未达到maxElements。
- 条件
- 内部操作:
- 每次循环调用内部的
dequeue()方法,该方法负责移除堆顶元素并执行下沉调整,同时将size减1。 - 将
dequeue()返回的元素添加到目标集合c中。这里要求集合c不能为null,否则抛出NullPointerException。
- 每次循环调用内部的
- 性能优势:相比反复调用
poll(),drainTo通过单次加锁、批量出队显著减少了锁获取和释放的开销,尤其在高并发场景下能有效提升吞吐量。 - 注意事项:
- 由于全程持锁,目标集合
c的add操作应尽可能快速(例如使用ArrayList并预估容量),以免长时间阻塞其他线程。 - 如果
maxElements设置过大,可能导致持锁时间过长,建议根据场景权衡。
- 由于全程持锁,目标集合
5. 实际应用场景与代码举例(JDK 8 兼容)
5.1 基础优先级任务调度
import java.util.concurrent.PriorityBlockingQueue;
public class BasicPriorityScheduler {
static class PriorityTask implements Comparable<PriorityTask> {
private final int priority;
private final String name;
public PriorityTask(int priority, String name) {
this.priority = priority;
this.name = name;
}
@Override
public int compareTo(PriorityTask o) {
return Integer.compare(this.priority, o.priority); // 数字越小优先级越高
}
public void run() {
System.out.println("Executing " + name + " with priority " + priority);
}
}
public static void main(String[] args) throws InterruptedException {
PriorityBlockingQueue<PriorityTask> queue = new PriorityBlockingQueue<>();
// 生产者线程
new Thread(() -> {
queue.put(new PriorityTask(3, "Task C"));
queue.put(new PriorityTask(1, "Task A (High)"));
queue.put(new PriorityTask(2, "Task B"));
}).start();
// 消费者线程
new Thread(() -> {
try {
while (true) {
PriorityTask task = queue.take();
task.run();
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
5.2 使用 Comparator 自定义排序
import java.util.Comparator;
import java.util.concurrent.PriorityBlockingQueue;
public class ComparatorExample {
static class Task {
private final int priority;
private final String name;
public Task(int priority, String name) {
this.priority = priority;
this.name = name;
}
public int getPriority() { return priority; }
public String getName() { return name; }
@Override
public String toString() {
return "Task{" + "priority=" + priority + ", name='" + name + '\'' + '}';
}
}
public static void main(String[] args) throws InterruptedException {
// 使用 Comparator 比较优先级,数字越小优先级越高
PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>(
11, Comparator.comparingInt(Task::getPriority)
);
queue.put(new Task(5, "Low"));
queue.put(new Task(1, "Critical"));
queue.put(new Task(3, "Medium"));
while (!queue.isEmpty()) {
System.out.println(queue.take()); // 按优先级输出
}
}
}
5.3 模拟生产者-消费者(无界队列的潜在风险)
import java.util.Random;
import java.util.concurrent.PriorityBlockingQueue;
public class UnboundedProducerConsumer {
public static void main(String[] args) {
PriorityBlockingQueue<Integer> queue = new PriorityBlockingQueue<>();
Random rand = new Random();
// 高速生产者
new Thread(() -> {
int count = 0;
while (true) {
int val = rand.nextInt(100);
queue.offer(val);
if (++count % 1000 == 0) {
System.out.println("Queue size: " + queue.size());
}
}
}).start();
// 慢速消费者
new Thread(() -> {
try {
while (true) {
Integer val = queue.take();
System.out.println("Consumed: " + val);
Thread.sleep(500); // 模拟慢消费
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
说明:运行一段时间后可观察到队列大小持续增长,若消费者长期慢于生产者,可能导致 OutOfMemoryError。
5.4 使用 drainTo 批量获取高优先级任务
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.PriorityBlockingQueue;
public class DrainToExample {
public static void main(String[] args) {
PriorityBlockingQueue<Integer> queue = new PriorityBlockingQueue<>();
queue.offer(30);
queue.offer(10);
queue.offer(20);
queue.offer(5);
List<Integer> batch = new ArrayList<>();
int drained = queue.drainTo(batch, 3);
System.out.println("Drained " + drained + " elements: " + batch);
System.out.println("Remaining in queue: " + queue);
}
}
5.5 线程池中使用 PriorityBlockingQueue
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class PriorityThreadPoolExample {
static class PriorityRunnable implements Runnable, Comparable<PriorityRunnable> {
private final int priority;
private final String name;
private static final AtomicInteger seq = new AtomicInteger(0);
public PriorityRunnable(int priority, String name) {
this.priority = priority;
this.name = name;
}
@Override
public int compareTo(PriorityRunnable o) {
return Integer.compare(this.priority, o.priority);
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +
" executing: " + name + " (priority=" + priority + ")");
}
}
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 2, 0L, TimeUnit.MILLISECONDS,
new PriorityBlockingQueue<>()
);
executor.execute(new PriorityRunnable(3, "Low Priority"));
executor.execute(new PriorityRunnable(1, "High Priority"));
executor.execute(new PriorityRunnable(2, "Medium Priority"));
executor.shutdown();
}
}
注意:FutureTask 本身不实现 Comparable,如果需要按优先级执行 Callable,需自定义 RunnableFuture 实现。
6. 吞吐量与性能分析
6.1 单锁设计对吞吐量的影响
PriorityBlockingQueue 使用单一 ReentrantLock 控制所有入队、出队操作。这与 ArrayBlockingQueue 类似,但与 LinkedBlockingQueue 的双锁设计不同。由于 put 永不阻塞,生产者持有锁的时间极短(仅元素插入和堆上浮),因此在高并发生产场景下,锁竞争相对较小。然而,消费者出队需要执行 siftDown(O(log n)),锁持有时间稍长。总体吞吐量在中等并发下表现良好,但在极高并发下,单一锁仍会成为瓶颈。
6.2 扩容开销分析
扩容是影响性能的关键因素:
- 扩容时需要释放主锁,允许其他线程继续操作,但 CAS 竞争
allocationSpinLock仍会有一定开销。 - 数组复制操作(
System.arraycopy)在容量较大时耗时显著。 - 扩容策略:容量小于 64 时,每次扩容增加当前容量 + 2;大于等于 64 时,每次扩容增加 50%。该策略在避免频繁扩容与节约内存之间取得平衡。
高并发下频繁扩容会严重影响性能,因此强烈建议根据预估元素数量设置合理的初始容量。
6.3 堆调整复杂度
PriorityBlockingQueue 的入队(上浮)和出队(下沉)操作时间复杂度为 O(log n),而 ArrayBlockingQueue 和 LinkedBlockingQueue 的入队/出队为 O(1)。当队列中元素数量很大(例如数十万)时,堆调整的成本不可忽视。因此,PriorityBlockingQueue 更适合中等规模、需优先级排序的场景,不适合极高吞吐量的简单 FIFO 队列。
6.4 内存占用
PriorityBlockingQueue 内部使用数组存储元素引用,相比 LinkedBlockingQueue 的链表节点(每个节点额外包含 next 指针),内存占用更紧凑。但扩容时可能会暂时浪费部分数组空间(例如容量翻倍后未满)。
6.5 对比 ArrayBlockingQueue 和 LinkedBlockingQueue
| 队列类型 | 排序开销 | 锁开销 | 适用场景 |
|---|---|---|---|
| PriorityBlockingQueue | O(log n) | 单锁,扩容可释放锁 | 需要按优先级处理任务 |
| ArrayBlockingQueue | O(1) | 单锁,满/空阻塞 | 固定容量、高吞吐量 FIFO |
| LinkedBlockingQueue | O(1) | 双锁,生产消费可部分并行 | 无界或大容量 FIFO,极高并发吞吐 |
6.6 性能调优建议
- 合理设置初始容量:预估最大元素数量,减少扩容次数。
- 使用
offer而非put:语义上无区别,但习惯使用offer更符合无界特性。 - 批量消费:使用
drainTo一次性取出多个元素,减少锁竞争次数。 - 避免存储海量元素:若元素数量可能超过数万,考虑使用其他数据结构(如分级队列、延迟处理)或采用有界队列配合拒绝策略。
- 考虑使用自定义 Comparator 代替自然顺序:避免元素类实现
Comparable时的额外装箱/拆箱(如果是基本类型包装类)。
7. 注意事项与常见陷阱
| 陷阱 / 注意事项 | 原因与说明 |
|---|---|
| 元素必须可比较 | 若未提供 Comparator,元素必须实现 Comparable。插入不可比较元素时会抛出 ClassCastException,因为堆调整需要比较大小。 |
不支持 null 元素 | null 被用作 poll() 等方法的特殊返回值,表示队列为空。插入 null 会立即抛出 NullPointerException。 |
| 无界可能导致内存溢出 | put / offer 永不阻塞,若消费者速度低于生产者,队列会无限增长,最终引发 OutOfMemoryError。生产环境中应监控队列大小,或封装为有界行为(例如继承并覆写 offer 在超过阈值时返回 false)。 |
remainingCapacity() 返回值无意义 | 总是返回 Integer.MAX_VALUE,不能用于判断队列是否可继续插入。 |
take() 的阻塞特性不受无界影响 | 即使队列无界,take() 在队列为空时仍然会阻塞,与有界队列行为一致。 |
| 迭代器弱一致性且不反映优先级顺序 | iterator() 返回的迭代器是弱一致性的,不保证遍历过程中队列的修改可见,且遍历顺序并非优先级顺序(仅是数组顺序)。若要按优先级遍历,应使用 take() 或 poll()。 |
drainTo 批量转移后的集合顺序 | drainTo 在持有锁期间反复调用 dequeue,因此转移到集合中的元素是按照优先级从高到低排列的。但若转移过程中发生中断或异常,部分元素可能已被移除。 |
| 扩容时的锁释放导致临时不一致 | tryGrow 中释放锁再扩容,期间其他线程可能修改 size 或进行入队/出队,导致 queue 与 size 短暂不一致。但通过 allocationSpinLock 和重获锁后的检查保证了最终正确性。普通使用者无需关心,但了解该细节有助于排查并发问题。 |
| 不支持公平性 | PriorityBlockingQueue 内部锁为非公平锁,且元素顺序由优先级决定,与线程等待时间无关。若需要按等待时间排序,可考虑 DelayQueue。 |
size() 方法的弱一致性 | size() 返回的 size 字段并非 volatile,在没有加锁的情况下读取可能不是最新值。尽管 JDK 文档未要求其强一致性,但在需要精确计数时,建议在锁保护下调用或使用原子方式。 |
8. 与其他阻塞队列的对比总结
| 特性 | PriorityBlockingQueue | ArrayBlockingQueue | LinkedBlockingQueue |
|---|---|---|---|
| 数据结构 | 数组二叉堆(最小堆) | 数组环形缓冲区 | 单向链表节点 |
| 有界性 | 无界(动态扩容,最大 Integer.MAX_VALUE - 8) | 有界,构造时固定容量 | 可选有界(默认无界,最大 Integer.MAX_VALUE) |
| 锁数量 | 1 个 ReentrantLock | 1 个 ReentrantLock | 2 个(putLock、takeLock) |
| 条件变量 | 仅 notEmpty | notEmpty、notFull | notEmpty、notFull(分别对应两把锁) |
| 是否支持优先级 | 是(通过自然顺序或 Comparator) | 否 | 否 |
| 生产阻塞(put) | 否(永不阻塞) | 是(队列满时阻塞) | 是(若指定容量且满时阻塞) |
| 消费阻塞(take) | 是(队列空时阻塞) | 是(队列空时阻塞) | 是(队列空时阻塞) |
| 扩容机制 | 动态数组扩容(CAS 竞争,释放锁扩容) | 固定容量,不可扩容 | 动态创建新节点,无扩容概念 |
| 插入/移除复杂度 | O(log n)(堆调整) | O(1) | O(1) |
| 内存占用 | 数组引用,扩容时可能浪费部分空间 | 固定数组,无额外节点开销 | 每个元素额外需要 Node 对象(含 next 指针) |
| 典型应用场景 | 优先级任务调度、带权重的消息处理 | 固定大小缓冲区、生产者-消费者速率匹配 | 高并发 FIFO 队列、线程池工作队列(Executors) |
9. 总结与学习指引
核心特点总结
PriorityBlockingQueue 是一个无界、支持优先级排序的阻塞队列,基于数组二叉堆实现。它的关键设计包括:
- 单锁 + 条件变量:使用一把
ReentrantLock保证线程安全,仅提供notEmpty条件用于消费者阻塞。 - 动态扩容:扩容时主动释放锁,通过 CAS 自旋锁控制并发扩容,减少锁竞争。
- 非对称阻塞:生产者永不阻塞,消费者在队列空时阻塞。
使用建议
- 适用场景:需要按优先级处理任务的系统(如作业调度、紧急消息优先处理)。
- 注意事项:必须监控队列大小,防止无界增长导致内存溢出;合理设置初始容量以降低扩容频率。
- 性能权衡:堆调整的 O(log n) 开销在元素数量巨大时会成为瓶颈,此时可考虑分级队列或改用 FIFO 队列。
- 替代方案:若需要延时执行,可考虑
DelayQueue;若需要零容量直接传递,可研究SynchronousQueue。