阻塞队列-5-PriorityBlockingQueue 详解

12 阅读26分钟

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 的主要区别

特性PriorityBlockingQueueArrayBlockingQueueLinkedBlockingQueue
有界性无界(动态扩容)有界(构造时固定容量)可选有界(默认 Integer.MAX_VALUE
排序特性按优先级排序(最小堆)FIFOFIFO
锁结构单锁 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 的核心字段如下:

// 二叉堆数组,存储队列元素,索引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;
}

流程解析

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

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

流程

  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 为例:

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 基础优先级任务调度

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),而 ArrayBlockingQueueLinkedBlockingQueue 的入队/出队为 O(1)。当队列中元素数量很大(例如数十万)时,堆调整的成本不可忽视。因此,PriorityBlockingQueue 更适合中等规模、需优先级排序的场景,不适合极高吞吐量的简单 FIFO 队列。

6.4 内存占用

PriorityBlockingQueue 内部使用数组存储元素引用,相比 LinkedBlockingQueue 的链表节点(每个节点额外包含 next 指针),内存占用更紧凑。但扩容时可能会暂时浪费部分数组空间(例如容量翻倍后未满)。

6.5 对比 ArrayBlockingQueue 和 LinkedBlockingQueue

队列类型排序开销锁开销适用场景
PriorityBlockingQueueO(log n)单锁,扩容可释放锁需要按优先级处理任务
ArrayBlockingQueueO(1)单锁,满/空阻塞固定容量、高吞吐量 FIFO
LinkedBlockingQueueO(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. 与其他阻塞队列的对比总结

特性PriorityBlockingQueueArrayBlockingQueueLinkedBlockingQueue
数据结构数组二叉堆(最小堆)数组环形缓冲区单向链表节点
有界性无界(动态扩容,最大 Integer.MAX_VALUE - 8有界,构造时固定容量可选有界(默认无界,最大 Integer.MAX_VALUE
锁数量1 个 ReentrantLock1 个 ReentrantLock2 个(putLocktakeLock
条件变量notEmptynotEmptynotFullnotEmptynotFull(分别对应两把锁)
是否支持优先级(通过自然顺序或 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