PriorityBlockingQueue 详解

1. 概述

PriorityBlockingQueue 是 Java 并发包(java.util.concurrent)中提供的一个无界阻塞队列 ,它支持按照元素的优先级 进行排序。与普通队列的 FIFO 规则不同,PriorityBlockingQueue 中的元素按照其自然顺序或者通过构造时提供的 Comparator 进行排序,队列的头部始终是当前优先级最高的元素(最小堆实现,即值最小的元素在堆顶,等价于最高优先级)。

核心特点

  • 无界队列 :理论上队列容量无限,插入元素(putoffer)永远不会阻塞。但内部基于数组存储,当元素数量超过当前数组长度时会触发动态扩容。
  • 优先级排序 :依赖二叉堆(默认最小堆)实现,元素必须可比较,要么实现 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 不阻塞(无界队列),直接插入,若容量不足则扩容 NullPointerExceptionClassCastException(如果元素不可比较)
offer(E e) e:元素 boolean:总是返回 true(无界) 不阻塞 NullPointerExceptionClassCastException
offer(E e, long timeout, TimeUnit unit) 元素、超时时间、时间单位 boolean:总是返回 true(超时参数被忽略) 不阻塞 NullPointerExceptionClassCastException
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;
}

流程解析

  1. 加锁(lock.lock())。
  2. 检查元素非 null
  3. size >= queue.length,调用 tryGrow 进行扩容(注意:扩容过程可能释放锁)。
  4. 执行 siftUp 将元素插入堆中合适位置。
  5. size 递增,并调用 notEmpty.signal() 唤醒可能阻塞在 take 的线程(仅当原来队列为空时唤醒有效,但此处无条件唤醒也无妨)。
  6. 解锁。

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;
    }
}

流程

  1. 若队列为空(size == 0),返回 nulltake 会在循环中调用 notEmpty.await() 阻塞。
  2. 取出堆顶元素 result = queue[0]
  3. 将数组最后一个元素 x = queue[n] 取出并置 null
  4. 调用 siftDown(0, x)x 下沉到合适位置,以维持最小堆性质。
  5. 更新 size,返回结果。

poll() 不阻塞,若队列空直接返回 null;超时版本使用 awaitNanos()

3.7 比较器支持

siftUpsiftDown 均有两个版本: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 序列化

序列化通过 writeObjectreadObject 方法实现。序列化时会将内部数组的元素转存到默认序列化机制中;反序列化时重新构建堆结构(调用 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]

详细说明

  1. 加锁offer 方法开始即调用 lock.lock(),确保后续对 sizequeue 的访问是线程安全的。
  2. 空值检查 :若 e == null,直接抛出 NullPointerException,因为队列不允许 null 元素。
  3. 容量检查与扩容
    • 判断当前元素数量 size 是否大于等于数组长度 queue.length
    • 若是,则调用 tryGrow 进行扩容。关键设计tryGrow 内部会先释放锁 ,然后通过 CAS 竞争 allocationSpinLock 来获得扩容权限。成功获得权限的线程分配新数组(新容量计算策略:若旧容量小于 64,增长 oldCap + 2;否则增长 oldCap >> 1 即 50%),再将旧数组元素拷贝到新数组,最后重新获取锁 ,更新 queue 引用。此过程允许其他线程在扩容期间继续入队/出队(前提是不需要扩容),提高了并发性能。
  4. 堆上浮调整 :扩容完成后(或无需扩容时),根据是否存在 comparator,分别调用 siftUpComparablesiftUpUsingComparator,将新元素从数组末尾(size 索引位置)上浮至正确位置,维持最小堆性质。
  5. 更新状态与唤醒size 自增。若插入前队列为空(size == 0),则通过 notEmpty.signal() 唤醒一个正在等待 take 的消费者线程。
  6. 解锁并返回 :释放锁,由于是无界队列,永远返回 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[返回堆顶元素]

详细说明

  1. 可中断加锁take() 使用 lock.lockInterruptibly() 获取锁,响应线程中断。
  2. 循环尝试出队
    • 调用内部方法 dequeue() 尝试从堆顶移除元素。dequeue() 的逻辑为:若 size == 0 则返回 null;否则取出 queue[0],将数组最后一个元素 x = queue[--size]null,并调用 siftDown(0, x)x 从堆顶下沉到合适位置,恢复堆序,最后返回原堆顶元素。
    • 如果 dequeue() 返回 null,说明队列为空,则当前线程调用 notEmpty.await() 进入条件等待队列,释放锁并阻塞。当其他线程插入元素并调用 notEmpty.signal() 时,该线程被唤醒,重新竞争锁,然后再次循环 尝试 dequeue
  3. 更新 size 与解锁dequeue 内部已执行 siftDown 并更新了 sizesize = 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 已持有锁
  • 步骤分解
    1. 释放锁 :线程 A 主动调用 lock.unlock(),释放全局锁,使得其他线程(如线程 B)可以继续操作队列(例如执行 take 减小 size,或执行无需扩容的 offer)。这是提高并发度的关键优化。
    2. CAS 竞争扩容权限 :线程 A 尝试用 CAS 将 allocationSpinLock 从 0 修改为 1。若成功,表明当前没有其他线程正在进行扩容,线程 A 获得扩容执行权;若失败,则说明已有线程在扩容,线程 A 会执行 Thread.yield() 让出 CPU 并稍后重新尝试获取锁(代码中会再次循环判断)。
    3. 分配新数组 :获得权限的线程 A 计算新容量(策略如前所述),并创建新的 Object[] newArray。完成后将 allocationSpinLock 重置为 0。
    4. 重新加锁并复制 :线程 A 调用 lock.lock() 重新获取锁(此时可能与其他线程竞争)。成功获取后,检查 queue 是否仍为旧数组(防止其他线程已完成扩容),若是,则将旧数组内容复制到新数组,并将 queue 引用指向新数组。
    5. 并发线程 B 的行为 :在线程 A 释放锁期间,线程 B 可能获得锁并尝试插入,同样发现需要扩容,于是进入 tryGrow。由于 allocationSpinLock 已被线程 A 占据,线程 B 的 CAS 失败,它将执行 Thread.yield(),然后重新竞争锁,等待线程 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([结束])

详细文字说明

  1. 功能目标 :当新元素 e 插入到二叉堆的末尾(索引 k)时,通过不断与父节点比较并交换,使新元素"上浮"到正确位置,维持最小堆性质(父节点 ≤ 子节点)。
  2. 比较器分支
    • 若构造队列时提供了 Comparator,则后续所有比较均调用 comparator.compare(e, parent)
    • 若未提供,则强制将 e 转换为 Comparable 并调用 e.compareTo(parent)。若元素未实现 Comparable,此处将抛出 ClassCastException
  3. 循环逻辑
    • 循环条件 k > 0:只要尚未到达堆顶(索引0),就继续比较。
    • 计算父节点索引:parent = (k - 1) >>> 1(无符号右移等效除以2)。
    • 比较 e 与父节点元素:若 e 小于 父节点,则违反最小堆规则,需将父节点元素复制到当前位置 array[k],并将 k 更新为父节点索引,继续向上比较。
    • e 大于等于父节点,则当前位置即为正确插入点,跳出循环。
  4. 最终放置 :将元素 e 放入 array[k]。该操作的时间复杂度为 O(log n),因为最多比较树的高度次。
  5. 源码对应 :此图精确对应 siftUpComparablesiftUpUsingComparator 方法的执行逻辑。

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

详细文字说明

  1. 功能目标 :在移除堆顶元素后,将数组最后一个元素 x 移动到索引 k(通常为0),然后通过与子节点比较并交换,使该元素"下沉"到合适位置,重建最小堆。
  2. 边界计算
    • half = size >>> 1 表示第一个叶子节点的索引。当 k >= half 时,说明 k 位置没有子节点(或已到达叶子层),无需再比较,直接将 x 放入该位置。
  3. 选择较小子节点
    • 计算左子节点索引 child = 2*k + 1,右子节点索引 right = child + 1
    • 根据是否存在 Comparator,比较左右子节点的值。如果 right < size(右子节点存在)且右子节点值小于 左子节点值,则将 child 指向右子节点。这一步确保 child 是较小的子节点。
  4. 比较与交换
    • 将待下沉元素 x 与较小的子节点 array[child] 比较。
    • x 小于等于 array[child],说明当前位置已满足堆性质,跳出循环。
    • 否则,将较小的子节点上浮 到当前位置 array[k] = array[child],并将 k 更新为 child 索引,继续向下比较。
  5. 最终放置 :循环结束后将 x 放入 array[k]。时间复杂度同为 O(log n)
  6. 源码对应 :此图精确对应 siftDownComparablesiftDownUsingComparator 方法。

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

详细文字说明

  1. 原子操作保证drainTo 从加锁开始到解锁结束,整个批量转移过程全程持有锁 。这确保了:
    • 队列在转移过程中不会被其他线程修改。
    • 转移出的元素严格遵循出队顺序,即优先级从高到低(最小堆堆顶优先)。
  2. 循环控制
    • 条件 n < maxElements:限制转移数量不超过指定上限。
    • 条件 size > 0:当队列中没有元素时立即终止,即使未达到 maxElements
  3. 内部操作
    • 每次循环调用内部的 dequeue() 方法,该方法负责移除堆顶元素并执行下沉调整,同时将 size 减1。
    • dequeue() 返回的元素添加到目标集合 c 中。这里要求集合 c 不能为 null,否则抛出 NullPointerException
  4. 性能优势 :相比反复调用 poll()drainTo 通过单次加锁、批量出队显著减少了锁获取和释放的开销,尤其在高并发场景下能有效提升吞吐量。
  5. 注意事项
    • 由于全程持锁,目标集合 cadd 操作应尽可能快速(例如使用 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) ,而 ArrayBlockingQueueLinkedBlockingQueue 的入队/出队为 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 性能调优建议

  1. 合理设置初始容量:预估最大元素数量,减少扩容次数。
  2. 使用 offer 而非 put :语义上无区别,但习惯使用 offer 更符合无界特性。
  3. 批量消费 :使用 drainTo 一次性取出多个元素,减少锁竞争次数。
  4. 避免存储海量元素:若元素数量可能超过数万,考虑使用其他数据结构(如分级队列、延迟处理)或采用有界队列配合拒绝策略。
  5. 考虑使用自定义 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 或进行入队/出队,导致 queuesize 短暂不一致。但通过 allocationSpinLock 和重获锁后的检查保证了最终正确性。普通使用者无需关心,但了解该细节有助于排查并发问题。
不支持公平性 PriorityBlockingQueue 内部锁为非公平锁,且元素顺序由优先级决定,与线程等待时间无关。若需要按等待时间排序,可考虑 DelayQueue
size() 方法的弱一致性 size() 返回的 size 字段并非 volatile,在没有加锁的情况下读取可能不是最新值。尽管 JDK 文档未要求其强一致性,但在需要精确计数时,建议在锁保护下调用或使用原子方式。

8. 与其他阻塞队列的对比总结

特性 PriorityBlockingQueue ArrayBlockingQueue LinkedBlockingQueue
数据结构 数组二叉堆(最小堆) 数组环形缓冲区 单向链表节点
有界性 无界(动态扩容,最大 Integer.MAX_VALUE - 8 有界,构造时固定容量 可选有界(默认无界,最大 Integer.MAX_VALUE
锁数量 1 个 ReentrantLock 1 个 ReentrantLock 2 个(putLocktakeLock
条件变量 notEmpty notEmptynotFull notEmptynotFull(分别对应两把锁)
是否支持优先级 (通过自然顺序或 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
相关推荐
shark22222222 小时前
Spring 的三种注入方式?
java·数据库·spring
陈煜的博客2 小时前
idea 项目只编译不打包,跳过测试,快速开发
java·ide·intellij-idea
JAVA学习通2 小时前
LangChain4j 与 Spring AI 的技术选型深度对比:2026 年 Java AI 工程化实践指南
java·人工智能·spring
.柒宇.2 小时前
Java八股之反射
java·开发语言
敖正炀2 小时前
LinkedTransferQueue 详解
java
环流_2 小时前
多线程1(面试题--常见的线程创建方式)
java·开发语言·面试
敖正炀3 小时前
ArrayBlockingQueue深度解析
java
敖正炀3 小时前
LinkedBlockingQueue详解
java
敖正炀3 小时前
SynchronousQueue 详解
java