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 的核心字段如下:
java
// 二叉堆数组,存储队列元素,索引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 构造器
java
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),因为永不阻塞。核心源码如下:
java
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 设计中的精妙之处:扩容时释放主锁,以减少锁竞争。
java
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() 阻塞获取的源码:
java
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() 方法负责从堆中移除堆顶并维持堆序:
java
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 为例:
java
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 类图详细说明
详细说明:
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 |
逻辑树结构:
详细说明:
- 该图展示了一个包含 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)流程图详细说明
详细说明:
- 加锁 :
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)流程图详细说明
详细说明:
- 可中断加锁 :
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)时序图详细说明
详细说明:
- 场景设定 :线程 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)操作详细流程图
详细文字说明:
- 功能目标 :当新元素
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)操作详细流程图
详细文字说明:
- 功能目标 :在移除堆顶元素后,将数组最后一个元素
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 批量消费流程图
详细文字说明:
- 原子操作保证 :
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 基础优先级任务调度
java
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 自定义排序
java
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 模拟生产者-消费者(无界队列的潜在风险)
java
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 批量获取高优先级任务
java
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
java
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。