BlockingQueue 与生产者-消费者模式:并发数据传递的源码内核(上)

40 阅读1小时+

前言:从“安全存储”到“安全传递”

在《Java 语言深度内核》系列的前文中,我们深入拆解了 ConcurrentHashMap 的源码全景,见证了它在 JDK 7 到 JDK 8 的演进中,如何通过 Segment 分段锁和 CAS + synchronized 的精妙设计,解决了高并发环境下安全存储键值对的难题。

然而,并发编程的艺术不仅在于“如何安全地存储”,更在于“如何安全地传递”。当两个线程需要协作完成一项任务时,BlockingQueue 是它们之间传递数据的最佳通道。ArrayBlockingQueue 用一把锁和两个条件实现了简单的阻塞等待,LinkedBlockingQueue 用双锁分离让入队和出队可以并行执行,SynchronousQueue 甚至不存储元素,直接将数据从生产者传递给消费者。这些队列的锁粒度选择和等待通知机制,与前文 ConcurrentHashMap 的并发设计一脉相承。

本文要回答的核心问题:

  • “为什么 ArrayBlockingQueue 用一把锁而 LinkedBlockingQueue 用两把锁?锁粒度的差异如何影响并发度?”
  • SynchronousQueue 没有容量,那么 puttake 究竟是如何配对的?公平和非公平模式背后的数据结构有何不同?”
  • DelayQueue 如何实现‘只有到期元素才能被取出’的语义?leader-follower 模式是如何优化多线程等待的?”
  • “线程池的 SynchronousQueue + CallerRunsPolicy 组合如何实现天然的限流和背压?”
  • “如果要设计一个百万级任务的延迟重试系统,DelayQueue 可行吗?如果不可行,应该如何优化?”

核心要点速览:

  • 接口契约BlockingQueue 定义了四组操作(抛异常、返回特殊值、阻塞、超时),put/take 的无限阻塞是其区别于普通 Queue 的本质。
  • ArrayBlockingQueue循环数组 + 单锁(ReentrantLock) + notEmpty/notFull 两个条件。入队出队完全互斥,简单公平,但高并发下锁竞争是瓶颈。
  • LinkedBlockingQueue单向链表 + 双锁分离(putLock/takeLock。入队和出队可以并行,吞吐量高,但内存开销更大,且不设防的无界容量可能引发 OOM。
  • SynchronousQueue零容量,元素不落地。通过**公平模式(队列)非公平模式(栈)**实现线程间的直接配对传递。
  • PriorityBlockingQueue无界数组 + 小顶堆take 永远返回优先级最高的元素,无阻塞入队。
  • DelayQueue委托 PriorityQueue,元素需实现 Delayed 接口,只有延迟到期后才能取出。通过 leader-follower 模式优化等待。

文章组织架构:

flowchart TD
    subgraph A ["认知路径"]
        direction LR
        A1[&#34;1. BlockingQueue 体系总览<br/>契约、架构与演进逻辑&#34;] --> A2[&#34;2. ArrayBlockingQueue<br/>单锁统治下的循环数组&#34;]
        A2 --> A3[&#34;3. LinkedBlockingQueue<br/>双锁分离下的高吞吐设计&#34;]
        A3 --> A4[&#34;4. SynchronousQueue<br/>零容量的配对艺术&#34;]
        A4 --> A5[&#34;5. PriorityBlockingQueue<br/>无界世界里的堆排序&#34;]
        A5 --> A6[&#34;6. DelayQueue<br/>时间契约驱动的延迟调度&#34;]
    end
    
    A6 --> B[&#34;7. 工程应用<br/>线程池任务队列 & RocketMQ 重试队列&#34;]
    B --> C[&#34;8. 系统设计实战<br/>基于 DelayQueue 的延迟重试组件&#34;]
    B -.- D[&#34;9. 面试高频专题&#34;]
    
    subgraph E [&#34;设计暗线&#34;]
        direction LR
        E1[&#34;锁粒度演进:单锁 → 双锁 → 无锁配对&#34;]
        E2[&#34;容量策略:有界 → 无界 → 零容量&#34;]
        E3[&#34;通知机制:signal → 跨锁 signal → 配对/leader-follower&#34;]
    end

    classDef nodeStyle fill:#f1f5f9,stroke:#334155,color:#1e293b
    classDef pathSub fill:#ede9fe,stroke:#8b5cf6,color:#4c1d95
    classDef externalSub fill:#fef3c7,stroke:#d97706,color:#92400e
    classDef designSub fill:#dbeafe,stroke:#2563eb,color:#1e3a8a

    class A1,A2,A3,A4,A5,A6 nodeStyle
    class B,C,D nodeStyle
    class E1,E2,E3 nodeStyle
    class A pathSub
    class E designSub
    class B,C,D externalSub

分层说明: 模块 1 建立体系认知,模块 2-6 沿“锁粒度从粗到细、容量从有界到无界再到零容量”的线索深挖五大实现类,模块 7-8 回归工程与系统设计,模块 9 以 12 道面试题收束全文。设计暗线贯穿始终:选择合适的 BlockingQueue,本质上是根据生产消费速率、内存容忍度和公平性要求,在锁粒度、容量策略和排序语义之间做出最优权衡


1. BlockingQueue 体系总览:契约、架构与演进逻辑

BlockingQueue 是 Java 并发包中连接“数据生产”与“数据消费”的主动脉。它不只是一个线程安全的容器,更是一套精密的线程协作协议——通过锁、条件变量和差异化的数据结构,在吞吐量、延迟、内存占用和公平性之间寻求最优平衡。本章将从宏观到微观,建立起 BlockingQueue 的完整认知框架:先通过 UML 类图鸟瞰其在集合框架中的位置,再拆解接口契约的四组操作与 AbstractQueue 骨架的模板方法模式,随后引入五维设计空间模型剖析所有实现类的核心权衡,最后以一张全景对比表和一条逻辑演进线收束,为后续各实现类的源码级深挖铺设统一的参照系。

1.1 类关系总图:三层分离的架构

deepseek_mermaid_20260618_375491.png

图 1-1 主旨概括:该 UML 类图以蓝(接口)、紫(抽象骨架)、绿(具体实现)三色分层,清晰展现了 BlockingQueue 在 Java 集合框架中的静态结构。五个实现类均继承 AbstractQueue 并实现 BlockingQueue,体现了“接口契约 + 骨架复用 + 差异化并发实现”的经典架构。

逐元素分解

  • 顶层接口链(蓝色)Iterable → Collection → Queue → BlockingQueue,语义逐步增强。Queue 定义非阻塞操作 offer/poll/peekBlockingQueue 进一步引入 put/take 阻塞方法和超时方法、批量操作 drainTo 及容量查询 remainingCapacity
  • 抽象骨架(紫色)AbstractCollection 提供集合通用骨架;AbstractQueue 基于 offer/poll/peek 实现了 add/remove/element 等方法的模板逻辑,是模板方法模式的典范。
  • 具体实现(绿色):五个类各自声明了核心私有字段,如 ArrayBlockingQueueitemslocknotEmptynotFullLinkedBlockingQueue 的双锁和原子计数器;DelayQueueleader 线程等,直观展示了它们在锁粒度、容量和通知机制上的设计差异。

设计原理映射:三层分离严格遵循接口隔离原则骨架实现模式BlockingQueue 作为最特化接口,只引入阻塞语义;AbstractQueue 提取公共队列操作;具体类通过不同的锁、条件和数据结构完成并发语义。这种设计使扩展新队列时只需聚焦核心的阻塞入队/出队逻辑。

工程联系与关键结论:阅读源码时,蓝色层指明契约,紫色层提供模板,绿色层揭示并发控制细节。自定义阻塞队列时,应继承 AbstractQueue 并实现 BlockingQueue,复用骨架代码,仅重写 put/take/offer/poll 及底层存储操作。

1.2 接口契约:四组操作与阻塞语义

BlockingQueue 为入队和出队各定义了四组方法,它们在面对“队列满”和“队列空”时采取截然不同的策略,形成了从“快速失败”到“无限等待”的完整操作谱系。

操作类型抛出异常返回特殊值无限阻塞超时阻塞
入队add(e)offer(e)put(e)offer(e, time, unit)
出队remove()poll()take()poll(time, unit)
查看element()peek()不支持不支持
  • 抛出异常组add(e) 在队列满时抛出 IllegalStateExceptionremove()element() 在队列空时抛出 NoSuchElementException。这组方法继承自 Collection/Queue 接口,将满/空视为程序错误,极少在生产者-消费者模式中使用。
  • 返回特殊值组offer(e) 满时返回 falsepoll()/peek() 空时返回 null。这是 Queue 接口的非阻塞操作基础,也是线程池任务提交流程的默认第一步试探。
  • 阻塞组put(e) 满时无限期阻塞当前线程,直到有空位;take() 空时无限期阻塞直到有元素。底层依赖 Condition.await(),线程在等待时释放锁并休眠,由对端线程在完成操作后通过 signal() 唤醒。这是 BlockingQueue 区别于普通 Queue 的本质特征。
  • 超时阻塞组offer(e, time, unit)poll(time, unit) 在满/空时最多阻塞指定时间,超时后返回 false/null。底层依赖 Condition.awaitNanos(long nanos),线程被超时唤醒后会重新检查条件,若不满足则返回。这组方法适用于对延迟敏感、不能无限等待的场景。
flowchart LR
    subgraph Insert [入队操作]
        direction TB
        I1[&#34;<b>add(e)</b><br/>满时抛异常&#34;]
        I2[&#34;<b>offer(e)</b><br/>满时返回 false&#34;]
        I3[&#34;<b>put(e)</b><br/>满时无限阻塞<br/>(Condition.await)&#34;]
        I4[&#34;<b>offer(e, time, unit)</b><br/>满时超时阻塞<br/>(Condition.awaitNanos)<br/>超时返回 false&#34;]
    end
    
    subgraph Remove [出队操作]
        direction TB
        R1[&#34;<b>remove()</b><br/>空时抛异常&#34;]
        R2[&#34;<b>poll()</b><br/>空时返回 null&#34;]
        R3[&#34;<b>take()</b><br/>空时无限阻塞<br/>(Condition.await)&#34;]
        R4[&#34;<b>poll(time, unit)</b><br/>空时超时阻塞<br/>(Condition.awaitNanos)<br/>超时返回 null&#34;]
    end
    
    style I3 fill:#f96,stroke:#333,stroke-width:4px
    style R3 fill:#f96,stroke:#333,stroke-width:4px

图 1-2 主旨概括:此图将入队与出队各四组操作并列展示,以高亮突出阻塞组,清晰区分它们对“满/空”状态的不同响应。阻塞组是 BlockingQueue 线程协作能力的核心来源。

设计原理映射:所有 BlockingQueue 实现类都继承自 AbstractQueue,后者利用模板方法模式将非阻塞操作(add/remove/element)建立在 offer/poll/peek 之上。子类只需重写 offer/poll/peek 以及阻塞/超时方法,即可自动获得完整的四组操作语义。这种分层使接口既全面又易于扩展。

工程联系与关键结论:在生产者-消费者模式中,生产者应使用 put 或超时 offer,消费者应使用 take 或超时 poll,绝不应使用可能抛异常的 add/remove。方法的选择实质上是面对不可控的速率差异时,对线程阻塞时间与系统延迟之间的一种策略性取舍。

null 元素的并发禁令BlockingQueue 不允许插入 null,任何 put(null)offer(null) 都会立即抛出 NullPointerException。这一设计根植于并发语义的确定性需求:poll()/peek() 返回 null 被统一解释为“队列为空”。若允许 null 元素,消费者在收到 null 时将无法区分“队列空”与“成功取出一个 null 值”,在并发环境下任何事后检查都存在不可弥补的竞态窗口。这与 ConcurrentHashMap 拒绝 null 键值的设计哲学完全一致,是并发容器的安全基石。

1.3 设计哲学:五维设计空间与核心权衡

任何 BlockingQueue 实现的选择,本质上是在五个核心维度上所做的权衡。这五个维度构成了解读所有实现类的统一坐标,也是演进分化的根本动力。

flowchart LR
    subgraph DesignSpace [&#34;BlockingQueue 五维设计空间&#34;]
        direction TB
        subgraph Dim1 [&#34;1. 底层数据结构&#34;]
            D1A[&#34;数组 (ArrayBlockingQueue)&#34;]
            D1B[&#34;单向链表 (LinkedBlockingQueue)&#34;]
            D1C[&#34;无存储 (SynchronousQueue)&#34;]
            D1D[&#34;二叉堆 (PriorityBlockingQueue / DelayQueue)&#34;]
        end
        subgraph Dim2 [&#34;2. 锁粒度&#34;]
            D2A[&#34;单锁 (Array / Priority / Delay)&#34;]
            D2B[&#34;双锁分离 (Linked)&#34;]
            D2C[&#34;无锁配对 (Synchronous)&#34;]
        end
        subgraph Dim3 [&#34;3. 容量策略&#34;]
            D3A[&#34;有界 (Array, 有限Linked)&#34;]
            D3B[&#34;无界 (Priority, Delay, 默认Linked)&#34;]
            D3C[&#34;零容量 (Synchronous)&#34;]
        end
        subgraph Dim4 [&#34;4. 通知机制&#34;]
            D4A[&#34;双条件精确唤醒 (notEmpty/notFull)&#34;]
            D4B[&#34;跨锁按需唤醒 (signalNotEmpty/NotFull)&#34;]
            D4C[&#34;线程配对 (TransferStack/Queue)&#34;]
            D4D[&#34;leader-follower 模式 (DelayQueue)&#34;]
        end
        subgraph Dim5 [&#34;5. 排序语义&#34;]
            D5A[&#34;FIFO (Array / Linked)&#34;]
            D5B[&#34;无顺序 (Synchronous)&#34;]
            D5C[&#34;优先级 (PriorityBlockingQueue)&#34;]
            D5D[&#34;延迟到期 (DelayQueue)&#34;]
        end
    end
    DesignSpace --> Summary[&#34;核心结论:没有银弹,只有权衡。<br/>选择合适的 BlockingQueue = 在五个维度上做出最优组合。&#34;]

图 1-3 主旨概括:五维设计空间模型将五个核心实现类映射到数据结构、锁粒度、容量、通知机制和排序语义五个轴上,直观展示了它们的不同选择。这五个维度是理解 BlockingQueue 家族设计基因的钥匙。

五维解析

  1. 底层数据结构:决定操作性能特征与内存占用。

    • 数组ArrayBlockingQueue):O(1) 随机访问,循环数组高效复用空间,无额外对象开销。
    • 单向链表LinkedBlockingQueue):动态插入删除无需数据搬移,但每个节点有额外内存开销(约 12-24 字节/节点)。
    • 无存储SynchronousQueue):不存任何元素,仅维护等待线程的排队结构。
    • 二叉堆PriorityBlockingQueue / DelayQueue):维护偏序关系(堆顶为最值),适合优先级和延迟语义。
  2. 锁粒度:决定并发度与竞争强度。

    • 单锁ArrayBlockingQueuePriorityBlockingQueueDelayQueue):所有操作串行化,实现简单但吞吐受限。
    • 双锁分离LinkedBlockingQueue):putLock 守护入队(操作尾节点),takeLock 守护出队(操作头节点),入队出队可并行,大幅提升吞吐。
    • 无锁配对SynchronousQueue):通过 CAS 操作完成线程间的直接数据交换,锁粒度趋近于零。
  3. 容量策略:决定背压能力与内存风险。

    • 有界(构造时指定容量):内存占用可预测,满时反向阻塞生产者,天然支持背压。
    • 无界Integer.MAX_VALUE 或自动扩容):生产者永不阻塞,但存在任务无限堆积导致 OOM 的风险。
    • 零容量:完全拒绝缓冲,生产者必须等待消费者配对,施加最强背压。
  4. 通知机制:定义线程间协作与唤醒的精确度。

    • 双条件精确唤醒notEmptynotFull 分别管理消费者和生产者的等待队列,出队后只唤醒生产者,入队后只唤醒消费者,避免无效竞争。
    • 跨锁按需唤醒LinkedBlockingQueue 在队列从空变非空(或满变非满)的边界时刻,需要临时获取对方锁来执行 signal,是双锁分离的必然代价但触发频率极低。
    • 线程直接配对:生产者与消费者直接通过栈或队列匹配,配对成功后互相唤醒,无传统条件变量。
    • leader-follower 模式DelayQueue 中仅 leader 线程按剩余延迟精确等待,其余 follower 无限休眠,极大减少超时唤醒次数。
  5. 排序语义:决定任务处理的公平性与优先级。

    • FIFO:严格按到达顺序出队,保证公平性。
    • 无顺序SynchronousQueue 直接传递,非公平模式下后到者可能先配对。
    • 优先级:按 Comparator 或自然顺序,堆顶始终是最优先元素。
    • 延迟到期:按 getDelay() 剩余时间排序,最早到期元素在堆顶,只有到期后才能取出。

1.4 家族全景对比

下表将五个核心实现类在上述五维空间中的具体落点进行了横向对比,可作为快速选型参考。

实现类语义特性底层结构锁机制容量排序通知机制典型场景
ArrayBlockingQueue有界 FIFO,支持公平锁Object[] 循环数组单锁 lock + notEmpty/notFull构造指定,固定FIFO双条件精确唤醒固定缓冲,需严格控制内存与公平性
LinkedBlockingQueue可选有界 FIFO,高吞吐单向链表 + 哨兵双锁 putLock/takeLock + 各自条件可选,默认 Integer.MAX_VALUEFIFO跨锁按需唤醒生产消费速率不匹配,需注意防 OOM
SynchronousQueue零容量直接传递无存储,公平队列 / 非公平栈基于 CAS 的配对零容量线程直接配对线程池背压,RPC 直接交接,消除排队
PriorityBlockingQueue无界优先级,堆顶最小Object[] 小顶堆单锁 + notEmpty(无 notFull无界,自动扩容优先级单条件精确唤醒VIP 任务调度,按优先级处理
DelayQueue无界延迟,到期才能取出委托 PriorityQueue单锁 + available 条件无界按到期时间leader-follower 模式定时任务,缓存过期,消息重试延迟

1.5 逻辑演进线:从基础阻塞到语义扩展

BlockingQueue 家族并非零散的创新集合,而是在面对不同并发需求时,沿着确定的方向在五维空间中持续调整和优化的结果。

flowchart TD
    ABQ[&#34;ArrayBlockingQueue<br/>有界·单锁·FIFO<br/>【基石】&#34;] -->|&#34;优化锁粒度<br/>提升吞吐&#34;| LBQ[&#34;LinkedBlockingQueue<br/>双锁分离·高吞吐&#34;]
    LBQ -->|&#34;极限容量策略<br/>消除缓冲&#34;| SQ[&#34;SynchronousQueue<br/>零容量·无锁配对<br/>【强背压】&#34;]
    ABQ -->|&#34;扩展排序语义<br/>赋予优先级&#34;| PBQ[&#34;PriorityBlockingQueue<br/>无界·单锁·优先级&#34;]
    PBQ -->|&#34;扩展时间语义<br/>赋予延迟&#34;| DQ[&#34;DelayQueue<br/>无界·单锁·延迟<br/>leader-follower&#34;]
    SQ -.->|&#34;同样可扩展&#34;| PBQ

演进路径解析

  1. 基石:ArrayBlockingQueue
    基于定长数组和单锁双条件,实现了最典型的阻塞队列管程模型。它奠定了 lock + notEmpty/notFull 的经典范式,是理解所有阻塞队列的起点。

  2. 第一次分化:锁粒度优化 → LinkedBlockingQueue
    为解决单锁互斥带来的吞吐瓶颈,利用链表入队(写尾)与出队(读头)无数据竞争的特性,将一把大锁拆分为 putLocktakeLock。入队出队得以并行,吞吐量跃升,但引入了额外的节点内存开销和边界条件下的跨锁唤醒。

  3. 第二次分化:容量策略极限 → SynchronousQueue
    当业务要求零缓冲、强反压时,有界和无界都不再适用。SynchronousQueue 将容量归零,通过队列(公平)或栈(非公平)管理等待线程,利用 CAS 实现直接配对传递。它完全消除了队列堆积,将压力真实反向传导给调用者。

  4. 第三次分化:排序语义扩展 → PriorityBlockingQueue / DelayQueue
    FIFO 无法满足优先级或定时需求。PriorityBlockingQueue 以小顶堆实现按优先级出队;DelayQueue 则按到期时间排序,并引入 leader-follower 模式优化多线程等待同一时间点时的无效唤醒。它们在锁粒度上回归单锁(堆操作需全局互斥),但在语义层将 BlockingQueue 从单纯的数据通道升格为任务调度的基础设施。

这条演进脉络清晰揭示:每一次分化,都是对一个或多个维度的定向突破——优化锁粒度、改变容量策略、扩展排序语义。整个家族由此构成了一个完整的多维解决方案矩阵,等待开发者根据具体场景在五个维度上找到最优落点。后续章节将对这五个实现类逐一展开源码级别的深挖。


2. ArrayBlockingQueue:单锁统治下的循环数组

ArrayBlockingQueueBlockingQueue 接口最经典、最直观的实现,也是理解阻塞队列工作原理的最佳起点。它基于固定长度的循环数组作为底层存储,使用单锁 ReentrantLock 配合两个条件变量 notEmptynotFull 构建了一套精确的阻塞/唤醒机制。本章将从数据结构、阻塞模型和核心操作三个层面,结合源码与时序图,彻底拆解 ArrayBlockingQueue 的内部运作。

2.1 特性与设计要点

  • 有界容量:构造时必须指定 capacity,数组一经创建长度不可变,内存占用可预测,天然支持流量控制。
  • FIFO 顺序:严格按生产者入队顺序出队,保证公平性。
  • 可选公平锁:构造器 ArrayBlockingQueue(int capacity, boolean fair) 可创建公平 ReentrantLock,等待线程按 FIFO 顺序获取锁,杜绝饥饿,但吞吐量降低。
  • 单锁互斥:入队与出队竞争同一把锁,任何时刻仅有一个线程能修改队列状态,实现简单可靠,但高并发下锁竞争成为瓶颈。
  • 双条件精确通知notEmpty 专门用于消费者等待“队列非空”,notFull 专门用于生产者等待“队列非满”。出队后唤醒生产者,入队后唤醒消费者,精准调度,避免无效竞争。

2.2 底层结构:循环数组与精确计数器

final Object[] items;          // 存储元素的定长数组
int takeIndex;                 // 下一次出队操作的索引
int putIndex;                  // 下一次入队操作的索引
int count;                     // 当前队列中的元素数量
final ReentrantLock lock;      // 全局唯一的锁
private final Condition notEmpty; // 队列非空条件
private final Condition notFull;  // 队列非满条件
  • items 数组final 修饰,容量不可变。元素在数组中循环存放,当 putIndextakeIndex 到达数组末尾时回绕到 0,形成一个逻辑上的环形缓冲区。
  • count 计数器:精确记录当前元素数量。判断“满”用 count == items.length,判断“空”用 count == 0。基于 count 的判断比计算 (putIndex + 1) % length == takeIndex 更直接,且在持有锁的前提下保证了强一致性。
  • lock 与两个条件lock 保护所有状态变量;notEmptynotFull 分别作为消费者和生产者的等待室。

2.3 阻塞模型:单锁 + 双条件的管程实现

ArrayBlockingQueue 的阻塞机制是 Java 管程(Monitor)模式在并发容器中的典范。它围绕一把全局锁、两个条件谓词和两个条件等待队列构建了一套精确的线程调度协议,其核心可概括为:获取锁 → 检查条件 → 条件不满足则等待 → 被唤醒后重新检查 → 执行操作并唤醒对端

(1)全局互斥与状态保护

ReentrantLock lock 是所有操作的唯一入口。任何线程在访问 itemstakeIndexputIndexcount 前都必须通过 lock.lockInterruptibly() 获取该锁。它同时承担三个职责:

  • 互斥:保证同一时刻只有一个线程能修改队列状态,消除数据竞争。
  • 内存可见性:锁的获取与释放触发 happens-before 关系,使线程间的状态修改立即可见。
  • 等待/通知的锚点Condition 对象必须绑定到锁上,线程必须在持有锁时才能调用 await()signal()

(2)条件谓词与角色分工

线程是否进入阻塞,取决于两个简明的布尔表达式:

  • 生产者阻塞条件count == items.length —— 队列已满。
  • 消费者阻塞条件count == 0 —— 队列为空。

这两个条件谓词将生产者和消费者严格分流:生产者只关心“是否有空位”,消费者只关心“是否有元素”。这种分工使得通知可以精准定向。

(3)双条件变量的精准唤醒

notFullnotEmpty 是从 lock 创建的两个独立 Condition 对象,各自维护一个 FIFO 等待队列:

条件变量等待者进入等待的时机被唤醒的时机
notFull生产者线程count == items.length 时调用 notFull.await()消费者完成 dequeue() 后调用 notFull.signal()
notEmpty消费者线程count == 0 时调用 notEmpty.await()生产者完成 enqueue() 后调用 notEmpty.signal()

synchronized 的单一等待集合相比,双条件变量的最大优势在于精准唤醒:消费者出队后只唤醒生产者,生产者入队后只唤醒消费者,不会出现“消费者出队后唤醒了另一个消费者”的无效竞争。这种设计将唤醒开销降到最低。

(4)while 循环:虚假唤醒的防线

所有阻塞方法在调用 await() 返回后,都必须在一个 while 循环中重新检查条件谓词:

while (count == items.length) {
    notFull.await();
}

这并非冗余代码,而是管程模型的强制要求。原因有二:

  • 虚假唤醒(spurious wakeup):操作系统可能在没有显式 signal() 的情况下唤醒线程,这是 Condition 规范明确允许的行为。
  • 多线程竞争:即使被正常唤醒,当多个生产者同时被 signal 时,只有一个能获取锁并成功入队,其余线程重新获取锁后会发现队列仍满,必须再次等待。

while 循环保证了无论因何种原因醒来,线程只有在条件真正满足时才继续执行。它是阻塞模型正确性的基石。

(5)await 的原子性:防止信号丢失的关键

notFull.await() 内部执行了三个不可分割的操作:

  1. 将当前线程加入 notFull 的条件等待队列;
  2. 释放 lock
  3. 挂起当前线程。

这三步是原子的,它保证:线程从检查条件到挂起的整个过程中,锁始终被持有,只有在完全准备好接收信号之后才释放锁。如果这三步不是原子的,假设线程在检查到队列满之后、加入等待队列之前释放了锁,这时消费者完成出队并调用 notFull.signal() 将无法找到该生产者(因为它尚未进入等待队列),导致信号丢失,生产者永远阻塞。await() 的原子性从根源上杜绝了这种丢失信号的竞态。

flowchart LR
    subgraph Queue[&#34;ArrayBlockingQueue 内部状态&#34;]
        Items[&#34;final Object[] items<br/>循环数组&#34;]
        Count[&#34;int count<br/>元素计数&#34;]
        PutIdx[&#34;putIndex&#34;]
        TakeIdx[&#34;takeIndex&#34;]
    end

    subgraph Lock[&#34;单锁与双条件&#34;]
        L[&#34;ReentrantLock lock&#34;]
        NCond[&#34;notFull Condition<br/>(生产者等待室)&#34;]
        ECond[&#34;notEmpty Condition<br/>(消费者等待室)&#34;]
        L --- NCond
        L --- ECond
    end

    Producer[&#34;生产者&#34;] -->|&#34;lockInterruptibly()&#34;| L
    Consumer[&#34;消费者&#34;] -->|&#34;lockInterruptibly()&#34;| L
    
    L -->|&#34;while(count==length)&#34;| NCond
    NCond -->|&#34;await():释放锁+挂起&#34;| Producer_Wait[&#34;生产者阻塞&#34;]
    Producer_Wait -.->|&#34;被 signal 唤醒<br/>重新竞争锁&#34;| L
    L -->|&#34;enqueue()&#34;| Items
    Items -->|&#34;notEmpty.signal()&#34;| ECond

    L -->|&#34;while(count==0)&#34;| ECond
    ECond -->|&#34;await():释放锁+挂起&#34;| Consumer_Wait[&#34;消费者阻塞&#34;]
    Consumer_Wait -.->|&#34;被 signal 唤醒<br/>重新竞争锁&#34;| L
    L -->|&#34;dequeue()&#34;| Items
    Items -->|&#34;notFull.signal()&#34;| NCond

图 2-1 主旨概括:此图完整呈现了 ArrayBlockingQueue单锁 + 双条件阻塞模型。所有线程必须通过唯一的 lock 进入临界区;生产者满时进入 notFull 等待室,消费者空时进入 notEmpty 等待室;操作完成后精准唤醒对端,形成闭环。

设计原理映射:该模型是 AQS 条件队列机制 的直接应用。lock 对应 AQS 同步器,notFullnotEmpty 各自对应一个条件队列。线程在 await 时从同步队列转移到条件队列,在 signal 时从条件队列转移回同步队列。这一转换过程正是管程模型在 Java 中的工程化实现。单锁不可避免——入队和出队不仅操作同一数组,还修改同一个 count 计数器,共享状态的粒度决定了锁的粒度。

工程联系与关键结论:理解这个模型的关键在于认识到,入队和出队不仅操作同一块内存区域(数组和索引),还操作同一个 count 计数器,因此必须互斥。单锁不是设计缺陷,而是数据结构特征决定的必然选择。高并发下,单锁模型会因线程频繁挂起/唤醒而损失吞吐量,这直接催生了下一章要讨论的 LinkedBlockingQueue 双锁分离设计。

2.4 核心操作:enqueue 与 dequeue

enqueuedequeue 是仅在持有锁时调用的私有方法,负责纯粹的数据移动和唤醒动作。

private void enqueue(E x) {
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length) putIndex = 0;
    count++;
    notEmpty.signal();
}

private E dequeue() {
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    items[takeIndex] = null;    // 避免内存泄漏
    if (++takeIndex == items.length) takeIndex = 0;
    count--;
    notFull.signal();
    return x;
}
  • 出队后的 items[takeIndex] = null 并非仅为逻辑清晰,而是防止被移除的元素因数组的强引用而无法被 GC 回收。
  • 每次入队都 notEmpty.signal(),每次出队都 notFull.signal(),即便可能触发少量无效唤醒,但被唤醒的线程会在 while 中重新检查条件,逻辑安全,代码简洁。

2.5 put / take 的完整阻塞流程(源码 + 时序)

public void put(E e) throws InterruptedException {
    Objects.requireNonNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}
sequenceDiagram
    participant P as 生产者
    participant L as lock
    participant NF as notFull
    participant Q as items
    participant NE as notEmpty
    participant C as 消费者

    Note over P,C: 初始状态:队列满 (count == capacity)

    P->>L: lockInterruptibly() 获取锁
    L-->>P: 获取成功
    P->>P: while(count == items.length) → true
    P->>NF: notFull.await()
    Note over P,NF: 原子操作:<br/>1. 加入 notFull 等待队列<br/>2. 释放 lock<br/>3. 挂起线程

    C->>L: lockInterruptibly() 获取锁
    L-->>C: 获取成功(生产者已释放锁)
    C->>Q: dequeue() 取出元素
    C->>NF: notFull.signal()
    Note over C,NF: 将生产者从 notFull 等待队列<br/>转移至 lock 的同步队列
    C->>L: unlock()

    P->>L: 生产者重新竞争锁成功
    P->>P: while(count == items.length) → false
    P->>Q: enqueue(e) 入队
    P->>NE: notEmpty.signal()
    P->>L: unlock()

图 2-2 主旨概括:此时序图以“队列满”为初始状态,完整展现了生产者因满阻塞、消费者出队后唤醒生产者、生产者重新获取锁并入队的全过程,重点刻画了 await() 内部“释放锁 + 挂起”的原子语义以及 signal() 后线程从条件队列到同步队列的转移。

逐元素分解

  • 生产者获取锁后发现队列满,调用 notFull.await(),原子性地将自己加入 notFull 的等待队列、释放锁并挂起。
  • 消费者获得锁(因生产者已释放),执行出队后调用 notFull.signal(),将 notFull 等待队列中的头节点(生产者)转移到 lock 的同步队列,使其等待锁释放后重新竞争。
  • 消费者释放锁后,生产者竞争到锁,从 await() 返回,在 while 中确认条件满足,完成入队,并反向唤醒可能阻塞在 notEmpty 上的消费者。

设计原理映射:该流程是 AQS 条件队列机制的经典应用。await() 的“释放锁+挂起”原子操作是管程模型的基石——它消除了“检查条件到挂起线程”之间的时间窗口,在这个窗口内锁必须被释放以允许对端线程改变条件。若这两步不是原子的,生产者可能在检查到队列满之后、挂起之前,消费者已经出队并 signal 了,但生产者还没进入等待队列,导致信号丢失、生产者永久阻塞。

工程联系与关键结论lockInterruptibly()while 循环分别对应两个关键的非功能性需求——可中断性(支持任务取消)和正确性(防御虚假唤醒)。在生产环境中,如果希望线程池关闭时阻塞在队列上的线程能响应中断退出,这两点是必需的保障。

2.6 超时版本 offer/poll

public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException {
    Objects.requireNonNull(e);
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length) {
            if (nanos <= 0L) return false;
            nanos = notFull.awaitNanos(nanos); // 返回剩余超时时间
        }
        enqueue(e);
        return true;
    } finally {
        lock.unlock();
    }
}

awaitNanos(long) 在提前唤醒时会返回剩余纳秒数,循环用剩余时间继续等待,确保总阻塞时间不会超过指定的超时阈值。

2.7 公平锁对吞吐的影响

构造器 fair=true 创建公平锁,严格按线程等待顺序分配锁,杜绝饥饿。但每次释放锁都需唤醒等待最久的线程,线程挂起和唤醒次数显著增加,无法利用 CPU 缓存的热点线程效应,吞吐量通常比非公平模式低数倍。公平锁仅适用于对 FIFO 顺序有严格要求的场景。

2.8 优点与局限性

优点

  • 内存占用可控,无额外节点开销,对 GC 友好。
  • 实现简洁,单锁模型易于理解、调试和维护。
  • 支持公平锁,可防止线程饥饿。
  • 有界容量天然提供背压,防止任务无限堆积。

局限性

  • 单锁互斥导致高并发下入队出队无法并行,吞吐量受限。
  • 容量固定,无法动态调整,缺乏弹性。
  • 公平锁代价高昂,大幅降低吞吐。
  • 仅支持 FIFO,无法扩展优先级或延迟语义。

2.9 Tips:为什么数组不能像链表那样做双锁分离?

在分析了 ArrayBlockingQueue 的单锁设计之后,你可能会产生一个疑问:既然 LinkedBlockingQueue 可以通过 putLocktakeLock 让入队与出队并行,那为何不把同样的思路用在数组上,构造一个“双锁+循环数组”的高吞吐有界队列?

表面原因:入队和出队都会修改 count 字段。链表的入队只改 last,出队只改 head,在队列非空非满时修改的字段完全正交;但数组的入队要执行 count++,出队要执行 count--,无论怎么拆分锁,count 始终是入队和出队的共享写热点。哪怕用 AtomicInteger 代替 int,也无法消除这个竞争。

根本原因count 的竞争会在 CPU 缓存层面引发缓存行乒乓(Cache Line Bouncing)。现代 CPU 以缓存行(通常 64 字节)为单位维护一致性,当一个核心写 count 时,另一个核心的对应缓存行会被立即失效。生产者每次 enqueuecount,消费者每次 dequeue 也写 count,它们所在的缓存行会不停地在两个核心之间弹跳,每次弹跳消耗数十到上百个 CPU 周期。这种物理层面的通信开销,足以抵消双锁并行带来的吞吐提升。

结论:锁粒度必须与数据结构的物理隔离度匹配。链表的头尾天然隔离,双锁是水到渠成;数组的 count 是物理上无法分割的共享热点,强行双锁只会得到一个“逻辑上并行、物理上乒乓”的尴尬设计。JDK 让 ArrayBlockingQueue 保持单锁的简洁可靠,把高吞吐的使命交给 LinkedBlockingQueue,正是对这一底层约束的深刻尊重。


3. LinkedBlockingQueue:双锁分离下的高吞吐设计

LinkedBlockingQueueBlockingQueue 接口的第二个经典实现,也是应对高并发场景的首选武器。它在 ArrayBlockingQueue 单锁模型的基础上,通过单向链表 + 双锁分离的设计,将入队和出队操作完全解耦,极大提升了吞吐量。本章将从定位与特性、底层结构、阻塞模型、核心操作和工程实践五个层面,逐层拆解 LinkedBlockingQueue 的内部运作。

3.1 定位、特性与适用场景

定位LinkedBlockingQueue以空间换并发度的典范。它牺牲了 ArrayBlockingQueue 的紧凑内存和简单实现,换取了入队与出队的真正并行,是 JUC 中面向高吞吐场景的标准阻塞队列。

核心特性

  • 单向链表,动态分配:底层为单向链表,节点在堆上动态创建和销毁,无需像数组那样预分配固定空间,天然支持弹性扩展。
  • 可选有界,默认无界:容量通过构造器指定,默认 Integer.MAX_VALUE(约 21 亿),近乎无界。也可显式设置有限容量,变为有界阻塞队列。
  • 双锁分离,入队出队并行:入队由 putLock 保护,出队由 takeLock 保护。只要队列非空非满,生产者和消费者互不阻塞,可同时进行。
  • FIFO 顺序:严格按生产者入队顺序出队,保证公平性。
  • 无公平锁支持:两把锁均使用非公平 ReentrantLock,追求最大吞吐,不支持类似 ArrayBlockingQueue 的公平模式。
  • AtomicInteger 全局计数:原子计数器作为两端的状态视图,避免锁分离后读取 count 时必须同时持有两把锁。
  • 边界条件下的跨锁唤醒:当队列从空变为非空或从满变为非满时,需要临时获取对方锁来唤醒等待线程,是双锁设计的必要代价,但触发频率极低。

适用场景

  • 高吞吐生产者-消费者模型:生产者和消费者速率不匹配、需要入队出队并行的场景,如日志异步落盘、消息中间件的本地缓冲。
  • 线程池任务队列ThreadPoolExecutorworkQueue 常用选择,尤其适合需要缓冲大量任务的 IO 密集型场景。必须显式设置有限容量,防止任务无限堆积导致 OOM。
  • 需要弹性容量的缓冲:在流量波动较大的系统中,链表动态分配的特性避免了预分配过大数组的内存浪费。
  • 批量数据流转:配合 drainTo() 方法,消费者可批量拉取元素,减少加锁次数,进一步提升吞吐。

不适用场景

  • 严格公平性要求:不支持公平锁,无法保证线程 FIFO 等待顺序,可能导致部分线程饥饿。
  • 内存极度敏感的系统:每个元素额外的 Node 对象开销在元素量极大时会造成显著内存压力。
  • 需要优先级或延迟语义:仅支持 FIFO,无法像 PriorityBlockingQueueDelayQueue 那样按优先级或到期时间出队。

3.2 底层结构:单向链表、哨兵节点与原子计数器

// LinkedBlockingQueue.java 核心域
static class Node<E> {
    E item;
    Node<E> next;
    Node(E x) { item = x; }
}
transient Node<E> head;                   // 头节点(哨兵,item 永远为 null)
private transient Node<E> last;           // 尾节点
private final int capacity;               // 容量
private final AtomicInteger count = new AtomicInteger(); // 原子计数器
transient final ReentrantLock takeLock = new ReentrantLock(); // 出队锁
private final Condition notEmpty = takeLock.newCondition();   // 非空条件
transient final ReentrantLock putLock = new ReentrantLock();   // 入队锁
private final Condition notFull = putLock.newCondition();     // 非满条件
  • Node 单向节点:每个元素封装为一个 Node 对象,包含 item 数据域和 next 指针。相比数组,链表省去了数据搬移开销,但每个节点有约 12~24 字节的额外内存占用。
  • head 哨兵节点:初始化时 head = last = new Node<E>(null)head.item 永远为 nullhead.next 指向第一个真实元素。哨兵的存在使得出队时只需移动 head 指针,无需处理“链表为空时出队”的特殊情况,简化了代码逻辑。
  • last 尾节点:指向链表最后一个真实节点(队列空时与 head 指向同一哨兵节点)。入队操作将新节点挂在 last.next 上,然后更新 last
  • capacity 容量:构造时指定,默认 Integer.MAX_VALUE。当 count 达到 capacity 时队列视为满。
  • AtomicInteger count:原子计数器,是双锁分离的关键基石。它使得入队侧和出队侧可以在不获取对方锁的情况下通过 count.get() 快速获知队列大致状态,从而决定是否继续操作或进行跨锁唤醒。
  • 两把锁与两个条件
    • putLock + notFull:守护入队操作和生产者等待。
    • takeLock + notEmpty:守护出队操作和消费者等待。

3.3 阻塞模型:双锁分离 + 跨锁信号传递

LinkedBlockingQueue 的阻塞模型建立在锁分离之上,其核心可概括为:入队线程竞争 putLock,出队线程竞争 takeLock;满时在 notFull 上等待,空时在 notEmpty 上等待;队列从空变非空或从满变非满时,需要短暂获取对方锁执行跨锁唤醒

3.3.1 组件架构图

flowchart TB
    subgraph Interface[&#34; 对外接口层 &#34;]
        direction LR
        PutAPI[&#34;<b>生产者入口</b><br/>put / offer / offer(time)&#34;]
        TakeAPI[&#34;<b>消费者入口</b><br/>take / poll / poll(time)&#34;]
    end

    subgraph LockLayer[&#34; 锁与条件层 &#34;]
        direction LR
        subgraph PutSide[&#34;入队侧&#34;]
            PutLock[&#34;<b>putLock</b><br/>ReentrantLock&#34;]
            NotFull[&#34;<b>notFull</b><br/>Condition<br/>等待谓词: count==capacity&#34;]
        end
        subgraph TakeSide[&#34;出队侧&#34;]
            TakeLock[&#34;<b>takeLock</b><br/>ReentrantLock&#34;]
            NotEmpty[&#34;<b>notEmpty</b><br/>Condition<br/>等待谓词: count==0&#34;]
        end
        PutLock --- NotFull
        TakeLock --- NotEmpty
    end

    subgraph StateLayer[&#34; 状态层 &#34;]
        Count[&#34;<b>AtomicInteger count</b><br/>原子元素计数&#34;]
        Capacity[&#34;<b>int capacity</b><br/>容量上限&#34;]
        Count --- Capacity
    end

    subgraph DataLayer[&#34; 数据存储层 &#34;]
        direction LR
        Head[&#34;<b>head</b><br/>哨兵节点<br/>item=null&#34;]
        Node1[&#34;Node<br/>item: A&#34;]
        Node2[&#34;Node<br/>item: B&#34;]
        Last[&#34;<b>last</b><br/>尾节点&#34;]
        Head --> Node1 --> Node2
        Last -.-> Node2
    end

    subgraph CrossSignal[&#34; 跨锁唤醒机制 &#34;]
        direction LR
        SigNE[&#34;<b>signalNotEmpty()</b><br/>由生产者调用<br/>获取 takeLock → notEmpty.signal()&#34;]
        SigNF[&#34;<b>signalNotFull()</b><br/>由消费者调用<br/>获取 putLock → notFull.signal()&#34;]
    end

    %% 连线关系
    PutAPI -->|&#34;1. 竞争锁&#34;| PutLock
    TakeAPI -->|&#34;1. 竞争锁&#34;| TakeLock
    PutLock -->|&#34;2. 入队后 count.getAndIncrement()&#34;| Count
    TakeLock -->|&#34;2. 出队后 count.getAndDecrement()&#34;| Count
    PutLock -.->|&#34;3. enqueue() 修改 last&#34;| Last
    TakeLock -.->|&#34;3. dequeue() 移动 head&#34;| Head
    NotFull -.->|&#34;满时 await()&#34;| PutAPI
    NotEmpty -.->|&#34;空时 await()&#34;| TakeAPI
    Count -.->|&#34;c == 0 触发&#34;| SigNE
    Count -.->|&#34;c == capacity 触发&#34;| SigNF
    SigNE -->|&#34;4. 跨锁唤醒&#34;| NotEmpty
    SigNF -->|&#34;4. 跨锁唤醒&#34;| NotFull
    PutLock -.->|&#34;未满时 signal()&#34;| NotFull
    TakeLock -.->|&#34;未空时 signal()&#34;| NotEmpty

    %% 配色
    classDef interfaceStyle fill:#dae8fc,stroke:#6c8ebf,stroke-width:2px,color:#000
    classDef lockStyle fill:#fff2cc,stroke:#d6b656,stroke-width:2px,color:#000
    classDef stateStyle fill:#d5e8d4,stroke:#82b366,stroke-width:2px,color:#000
    classDef dataStyle fill:#e1d5e7,stroke:#9673a6,stroke-width:2px,color:#000
    classDef crossStyle fill:#f8cecc,stroke:#b85450,stroke-width:2px,color:#000

    class PutAPI,TakeAPI interfaceStyle
    class PutLock,TakeLock,NotFull,NotEmpty lockStyle
    class Count,Capacity stateStyle
    class Head,Node1,Node2,Last dataStyle
    class SigNE,SigNF crossStyle

图 3-2 主旨概括:该架构图将 LinkedBlockingQueue 的阻塞模型划分为**对外接口(蓝)、锁与条件(黄)、状态管理(绿)、数据存储(紫)、跨锁唤醒(红)**五个层次。连线明确标注了从接口调用、锁竞争、状态更新、链表操作到跨层唤醒的完整路径,直观揭示双锁分离下的组件协作与数据流向。

连线关系逐条说明

连线含义
生产者入口 → putLock生产者调用 put/offer 时,必须先通过 putLock.lockInterruptibly() 获取入队锁。
消费者入口 → takeLock消费者调用 take/poll 时,必须先通过 takeLock.lockInterruptibly() 获取出队锁。
putLock → count生产者成功入队后,调用 count.getAndIncrement() 原子递增计数。
takeLock → count消费者成功出队后,调用 count.getAndDecrement() 原子递减计数。
putLock → last持有 putLock 的生产者通过 enqueue() 修改尾节点 last 指针,将新节点接入链表。
takeLock → head持有 takeLock 的消费者通过 dequeue() 移动头节点 head 指针,移除旧哨兵。
notFull → 生产者入口当队列满时,生产者调用 notFull.await() 释放 putLock 并进入阻塞。
notEmpty → 消费者入口当队列空时,消费者调用 notEmpty.await() 释放 takeLock 并进入阻塞。
count → signalNotEmpty生产者在入队后,若 c == 0(入队前队列为空),触发跨锁唤醒。
count → signalNotFull消费者在出队后,若 c == capacity(出队前队列为满),触发跨锁唤醒。
signalNotEmpty → notEmpty生产者临时获取 takeLock,调用 notEmpty.signal() 唤醒阻塞的消费者。
signalNotFull → notFull消费者临时获取 putLock,调用 notFull.signal() 唤醒阻塞的生产者。
putLock → notFull生产者入队后若队列仍未满,调用 notFull.signal() 唤醒下一个等待的生产者。
takeLock → notEmpty消费者出队后若队列仍有元素,调用 notEmpty.signal() 唤醒下一个等待的消费者。

设计原理映射:该布局将生产者与消费者的控制路径水平分离,共享状态与数据层居中纵向贯穿,跨锁唤醒机制独立于底部。连线编号与标注展示了“获取锁 → 操作数据 → 更新状态 → 判断是否唤醒”的完整时序闭环。五色分层确保每个关注面在视觉上独立,便于审查和调试。

工程联系与关键结论:阅读源码时,可按颜色快速跳转:蓝色入口 → 黄色锁等待 → 绿色状态判断 → 紫色链表操作 → 红色跨锁通知。这种结构化认知能显著加快对高并发吞吐队列的排错与调优效率。

3.3.2 阻塞与通知的详细流程

(1)常规路径下的完全并行

当队列既不空也不满(0 < count < capacity)时,生产者和消费者操作的是链表的不同字段,且各自持有不同的锁:

  • 生产者持有 putLock,修改 last.nextlast,原子递增 count
  • 消费者持有 takeLock,修改 headhead.next,原子递减 count

两者在代码路径上完全无交集,锁也无交集,因此可以真正并行执行。这是 LinkedBlockingQueue 吞吐量远高于 ArrayBlockingQueue 的根本原因。

(2)生产者阻塞与通知路径

当队列已满(count == capacity)时,生产者线程进入阻塞路径:

  1. 生产者通过 putLock.lockInterruptibly() 获取入队锁。
  2. while 循环中检查 count.get() == capacity,条件为 true
  3. 调用 notFull.await():原子地将当前线程加入 notFull 的条件等待队列,释放 putLock,线程挂起。
  4. 此后某个消费者完成出队,发现出队前队列为满(c == capacity),调用 signalNotFull()
    • putLock.lock() 获取入队锁(此时消费者尚持有 takeLock,这是一个跨锁获取操作)。
    • notFull.signal()notFull 等待队列中的头节点(阻塞的生产者)转移到 putLock 的 AQS 同步队列。
    • putLock.unlock() 释放入队锁。
  5. 被唤醒的生产者重新竞争 putLock,获取锁后从 await() 返回。
  6. while 循环中重新检查 count,确认有空位后调用 enqueue(node) 完成入队。
  7. 调用 count.getAndIncrement() 获取入队前元素数 c
  8. c == 0(入队前队列为空),调用 signalNotEmpty() 跨锁唤醒等待的消费者。
  9. c + 1 < capacity(入队后仍未满),调用 notFull.signal() 唤醒下一个可能等待的生产者。
(3)消费者阻塞与通知路径

消费者阻塞路径与生产者对偶:

  1. 消费者通过 takeLock.lockInterruptibly() 获取出队锁。
  2. while 循环中检查 count.get() == 0,条件为 true
  3. 调用 notEmpty.await():将线程加入 notEmpty 的条件等待队列,释放 takeLock,挂起。
  4. 此后某个生产者完成入队,发现入队前队列为空(c == 0),调用 signalNotEmpty()
    • takeLock.lock() 跨锁获取出队锁。
    • notEmpty.signal() 将消费者从条件队列转移到 takeLock 的同步队列。
    • takeLock.unlock() 释放出队锁。
  5. 消费者竞争 takeLock 成功后从 await() 返回,重新检查 count,确认有元素后调用 dequeue() 完成出队。
  6. 调用 count.getAndDecrement() 获取出队前元素数 c
  7. c == capacity(出队前队列满),调用 signalNotFull() 跨锁唤醒等待的生产者。
  8. c > 1(出队后仍有元素),调用 notEmpty.signal() 唤醒下一个可能等待的消费者。
(4)跨锁唤醒的深层分析

跨锁唤醒是 LinkedBlockingQueue 设计中最精妙、也最容易引起疑问的地方。以下从三个维度深入分析:

a) 为什么必须跨锁?

Condition 的规范要求:调用 signal() 的线程必须持有该 Condition 关联的锁。notEmpty 关联的是 takeLock,生产者要唤醒在 notEmpty 上等待的消费者,必须先获取 takeLock。这是 AQS 框架的硬性约束,不是设计选项。

b) 跨锁唤醒是否会破坏线程安全?

不会。原因在于跨锁唤醒的发生时机:

  • signalNotEmpty() 由生产者在入队完成后调用,此时生产者已经释放了 putLockfinally { putLock.unlock() }signalNotEmpty() 之前执行)。
  • 因此生产者在调用 takeLock.lock() 时,已经不再持有 putLock,不存在死锁风险。
  • 消费者侧的 signalNotFull() 同理,在 takeLock 释放后才获取 putLock

c) 跨锁唤醒的性能代价有多大?

极小。跨锁唤醒仅在队列状态发生根本性转变时触发:

  • signalNotEmpty() 仅在 c == 0 时调用——即入队前队列为空,入队后变为非空。
  • signalNotFull() 仅在 c == capacity 时调用——即出队前队列为满,出队后变为非满。

在稳定运行状态下,这两种边界条件的触发频率远低于常规的入队出队操作。绝大多数时候,入队发现队列非空(c > 0),不会调用 signalNotEmpty();出队发现队列非满(c < capacity),不会调用 signalNotFull()。跨锁唤醒的开销被均摊到大量常规操作上,对整体吞吐的影响可以忽略不计。

3.3.3 与 ArrayBlockingQueue 阻塞模型的对比
维度ArrayBlockingQueueLinkedBlockingQueue
锁数量1把 lock2把 putLock + takeLock
条件变量notEmpty + notFull 绑定同一锁notEmpty 绑定 takeLocknotFull 绑定 putLock
入队出队关系完全互斥非空非满时可并行
计数器int count(锁保护)AtomicInteger count(无锁读)
通知方式入队后直接 notEmpty.signal()入队后若 c==0,需跨锁获取 takeLocknotEmpty.signal()
信号丢失风险无(同一把锁保护整个流程)无(await() 的原子性 + c 值判断保证时序正确)

设计原理映射LinkedBlockingQueue 的阻塞模型是锁分离 + 最低限度耦合原则的工程实践。它先通过分析数据结构的物理隔离度,将一把大锁拆为两把小锁;再针对拆锁后的边界通信需求,用条件触发的跨锁唤醒予以解决。这种“能分离则分离,必须耦合才耦合”的设计哲学,是并发程序设计中提升伸缩性的经典范式。

3.4 核心操作:enqueue 与 dequeue

// 入队:仅在持有 putLock 时调用
private void enqueue(Node<E> node) {
    last = last.next = node;    // 将新节点挂在链表尾部,更新 last 指针
}

// 出队:仅在持有 takeLock 时调用
private E dequeue() {
    Node<E> h = head;
    Node<E> first = h.next;
    h.next = h;                // 断开哨兵节点的 next 指针,帮助 GC
    head = first;
    E x = first.item;
    first.item = null;         // 新哨兵节点 item 置 null
    return x;
}
  • enqueue 极为简洁,仅修改 last.nextlast 指针,O(1) 完成。
  • dequeueh.next = h 是自引用的技巧,用于断开旧哨兵节点与链表的联系,帮助 GC 回收。被移除元素成为新的哨兵节点,其 item 被置 null,标记为已出队。

3.5 put / take 的完整阻塞流程(源码 + 时序)

// put 方法
public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        while (count.get() == capacity) {
            notFull.await();            // 队列满,在 notFull 上等待
        }
        enqueue(node);                  // 入队
        c = count.getAndIncrement();    // 获取入队前元素数
        if (c + 1 < capacity)
            notFull.signal();           // 入队后仍未满,唤醒下一个生产者
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();               // 入队前队列为空,跨锁唤醒消费者
}

// take 方法
public E take() throws InterruptedException {
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        while (count.get() == 0) {
            notEmpty.await();           // 队列空,在 notEmpty 上等待
        }
        E x = dequeue();               // 出队
        c = count.getAndDecrement();   // 获取出队前元素数
        if (c > 1)
            notEmpty.signal();         // 出队后仍有元素,唤醒下一个消费者
        return x;
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();               // 出队前队列满,跨锁唤醒生产者
}

关键细节

  • while 循环与 ArrayBlockingQueue 一致,是防御虚假唤醒的必需。
  • c = count.getAndIncrement() 返回的是入队前的元素数,用于后续判断是否需要跨锁唤醒。
  • notFull.signal()notEmpty.signal() 分别在入队后未满、出队后非空时唤醒同侧的下一个等待线程,减少无效唤醒。
sequenceDiagram
    participant P as 生产者
    participant PL as putLock
    participant NF as notFull
    participant Q as 链表
    participant Cnt as count
    participant TL as takeLock
    participant NE as notEmpty
    participant C as 消费者

    Note over P,C: 初始状态:队列满 (count == capacity)

    P->>PL: lockInterruptibly()
    PL-->>P: 获取 putLock
    P->>Cnt: count.get() == capacity → true
    P->>NF: notFull.await()
    Note over P,NF: 生产者释放 putLock 并阻塞

    C->>TL: lockInterruptibly()
    TL-->>C: 获取 takeLock
    C->>Q: dequeue() 取出元素
    C->>Cnt: c = count.getAndDecrement() → c == capacity
    C->>TL: unlock()

    Note over C,PL: 出队前队列满,跨锁唤醒生产者
    C->>PL: lock() 获取 putLock
    PL-->>C: 获取成功
    C->>NF: notFull.signal()
    C->>PL: unlock()

    NF-->>P: 生产者被唤醒,重新竞争 putLock
    P->>PL: 重新获取 putLock 成功
    P->>P: while(count.get() == capacity) → false
    P->>Q: enqueue(node) 入队
    P->>Cnt: c = count.getAndIncrement() → c 为入队前 count
    P->>PL: unlock()
    
    opt c == 0
        P->>TL: lock() 获取 takeLock
        P->>NE: notEmpty.signal()
        P->>TL: unlock()
    end

图 3-3 主旨概括:此时序图以“队列满”为初始状态,完整呈现了生产者因满阻塞、消费者出队后跨锁唤醒生产者、生产者重新获取锁并入队的全过程。重点刻画了跨锁唤醒的时序——消费者在释放 takeLock 之后才获取 putLock 执行 signal

逐元素分解

  • 生产者获取 putLock 后发现队列满,notFull.await() 释放锁并挂起。
  • 消费者获取 takeLock,出队后通过 c == capacity 判断出队前队列满,释放 takeLock,然后跨锁获取 putLock,执行 notFull.signal(),唤醒生产者。
  • 生产者重新竞争 putLock 成功后继续执行,入队后若 c == 0(入队前队列空),则跨锁获取 takeLock 并执行 notEmpty.signal(),唤醒可能等待的消费者。

3.6 超时版本 offer/poll

ArrayBlockingQueue 的超时机制类似,通过 awaitNanos(long) 实现限时等待,循环用剩余超时时间继续等待,直至超时耗尽或条件满足。

3.7 优点与局限性

优点

  • 高吞吐:双锁分离使入队和出队可并行执行,在高并发、生产消费速率不匹配的场景下吞吐量远超 ArrayBlockingQueue
  • 弹性容量:链表结构天然支持动态增长,且可通过 capacity 参数在有界和无界之间灵活切换。
  • 批量操作高效drainTo() 方法可一次性批量移出元素,减少加锁次数,适合批量消费场景。

局限性

  • 内存开销大:每个元素额外需要一个 Node 对象(约 12~24 字节),百万级元素时额外内存可达数十 MB。
  • 默认无界容量危险:无参构造器默认 Integer.MAX_VALUE,若消费者速率持续低于生产者,任务会在链表中无限堆积,最终导致 OOM。生产环境中必须显式设置合理容量
  • 不支持公平锁:双锁均为非公平锁,无法保证 FIFO 等待顺序,极端情况下可能导致线程饥饿。
  • 跨锁唤醒的短暂开销:虽然触发频率低,但每次状态转换都涉及两次额外的锁操作,在高频边界场景下可能产生微弱影响。

4. SynchronousQueue:零容量的配对艺术

SynchronousQueueBlockingQueue 家族中最特殊的一个实现。它彻底抛弃了“存储元素”这一传统队列的根基,转而实现了一种线程间的直接数据传递。生产者的 put 操作必须等待消费者的 take 操作,反之亦然。这是一种“一手交钱,一手交货”的零库存模式。本章将从定位与特性、底层结构、公平/非公平配对机制、核心 transfer 方法实现、通知机制、以及优缺点与工程应用等维度,深入剖析这个看似简单、实则精巧的并发组件。

4.1 定位、特性与适用场景

定位SynchronousQueue零容量、强背压的直接传输通道。它不存储任何元素,每个插入操作都必须等待另一个线程的移除操作,反之亦然。它是将并发压力真实反向传导给调用方的终极工具。

核心特性

  • 零容量isEmpty() 永远返回 truesize() 永远返回 0peek() 永远返回 null。队列内部完全不保留任何元素。
  • 直接传递:数据直接从生产者线程传递到消费者线程,中间不经过任何缓冲区。
  • 公平/非公平配对可选:构造器 SynchronousQueue(boolean fair) 指定配对策略。公平模式下,等待最久的线程优先配对(FIFO);非公平模式下,后到达的线程优先配对(LIFO),默认非公平以追求更高吞吐。
  • 无锁节点配对:内部通过 Transferer 抽象类的 CAS 操作完成线程节点的入队/出栈和配对,避免了重量级锁。
  • 强制背压:由于没有缓冲,生产者必须等待消费者就绪,系统负载压力会被立刻、真实地传递给上游调用者。

适用场景

  • 线程池中的直接交接Executors.newCachedThreadPool() 底层就使用 SynchronousQueue。新任务到来时,如果没有空闲线程,则立即创建新线程;如果线程数已达上限且无空闲线程,则触发拒绝策略,实现任务不积压。
  • CallerRunsPolicy 组合实现限流:当线程池满负荷且队列满(SynchronousQueue 总是满)时,offer 立即失败并执行 CallerRunsPolicy,让调用者线程自行执行任务,从而自然减缓了任务提交速率,形成背压。
  • RPC 框架的即发即收:在某些高性能 RPC 框架中,请求和响应需要严格的一对一绑定,SynchronousQueue 可以充当请求-响应的交接点。
  • 需要严格速率匹配的场景:生产者和消费者速率必须匹配,否则任一方都会被阻塞,适用于对延迟和堆积极度敏感的系统。

不适用场景

  • 需要缓冲流量的系统:生产者和消费者速率天然不匹配且需要削峰填谷时,应使用有界或无界队列。
  • 生产者不愿意阻塞:如果生产者线程是宝贵资源(如 Web 容器的 IO 线程),不能因为消费者慢而被阻塞,则不适合使用 SynchronousQueue
  • 任务执行时间差异大:长任务会长时间阻塞生产者,降低系统吞吐。

4.2 底层结构:Transferer 抽象与两种实现

SynchronousQueue 的核心是一个名为 Transferer 的内部抽象类,它只有一个方法:

abstract static class Transferer<E> {
    abstract E transfer(E e, boolean timed, long nanos);
}

所有入队和出队操作都转化为对该方法的调用:

  • 生产者调用 transfer(e, timed, nanos),交出元素 e,等待消费者接收,返回值为对端传回的值(通常为 null)。
  • 消费者调用 transfer(null, timed, nanos),表示索取一个元素,返回值为生产者传入的元素。

Transferer 有两个具体实现,由构造器的 fair 参数决定:

  • TransferQueue<E>:公平模式。内部维护一个 FIFO 队列,等待的线程节点按到达顺序排队,队头优先匹配。
  • TransferStack<E>:非公平模式(默认)。内部维护一个 LIFO 栈,后到达的线程节点优先匹配,利用 CPU 缓存的热线程效应,通常吞吐量更高,但可能导致等待最久的线程饥饿。
classDiagram
    class SynchronousQueue {
        -Transferer transferer
        +SynchronousQueue(boolean fair)
        +put(E e)
        +take() E
    }
    class Transferer {
        <<abstract>>
        +transfer(E e, boolean timed, long nanos) E
    }
    class TransferQueue {
        -QNode head
        -QNode tail
        +transfer(E e, boolean timed, long nanos) E
    }
    class TransferStack {
        -SNode head
        +transfer(E e, boolean timed, long nanos) E
    }
    class QNode {
        E item
        boolean isData
        QNode next
        Thread waiter
    }
    class SNode {
        E item
        int mode
        SNode next
        SNode match
        Thread waiter
    }

    Transferer <|-- TransferQueue
    Transferer <|-- TransferStack
    SynchronousQueue --> Transferer
    TransferQueue --> QNode
    TransferStack --> SNode

图 4-1 主旨概括:此 UML 类图展示了 SynchronousQueue 的核心结构:它持有 Transferer 引用,公平模式使用 TransferQueue(基于队列节点 QNode),非公平模式使用 TransferStack(基于栈节点 SNode)。所有的阻塞、配对逻辑都封装在 transfer 方法中。

逐元素分解

  • SynchronousQueue:对外暴露 puttake 等阻塞队列 API,内部委托给 Transferer.transfer()
  • Transferer:抽象类,定义配对协议。参数 enull 表示消费者请求,非 null 表示生产者提交。
  • TransferQueue:公平实现,内部维护 QNode 双向队列。节点包含 itemisData(生产者标记)、waiter(等待线程)。
  • TransferStack:非公平实现,内部维护 SNode 栈。节点额外包含 mode(请求/数据)和 match(指向配对节点),以支持取消和自旋等待。
  • QNode / SNode:线程节点,保存数据或空标记,以及阻塞的线程引用,通过 LockSupport.park/unpark 进行挂起和唤醒。

4.3 阻塞模型:公平(队列)与非公平(栈)的配对逻辑

SynchronousQueue 的阻塞模型颠覆了传统队列的“存储-取出”范式,转而实现“等待-配对”模式。线程到达时,首先检查是否有互补模式的线程正在等待,若有则立即配对并交换数据;若没有,则将自己挂起,等待后续线程的到来。

4.3.1 组件架构与配对流程图

flowchart TB
    subgraph API[&#34; 对外接口 &#34;]
        Put[&#34;put(e)&#34;]
        Take[&#34;take()&#34;]
    end

    subgraph Core[&#34; 配对引擎 &#34;]
        Transfer[&#34;<b>Transferer.transfer(e, timed, nanos)</b>&#34;]
        CheckMode{&#34;e == null ?<br/>(消费者)&#34;}
    end

    subgraph FairImpl[&#34; 公平实现 TransferQueue &#34;]
        direction LR
        QHead[&#34;队头<br/>等待最久&#34;]
        QT1[&#34;QNode<br/>DATA&#34;]
        QT2[&#34;QNode<br/>REQUEST&#34;]
        QTail[&#34;队尾<br/>最新到达&#34;]
    end

    subgraph NonFairImpl[&#34; 非公平实现 TransferStack &#34;]
        direction LR
        STop[&#34;栈顶<br/>最新到达&#34;]
        ST1[&#34;SNode<br/>DATA&#34;]
        ST2[&#34;SNode<br/>REQUEST&#34;]
        SBottom[&#34;栈底<br/>等待最久&#34;]
    end

    Put --> Transfer
    Take --> Transfer
    Transfer --> CheckMode
    CheckMode -->|&#34;公平&#34;| FairImpl
    CheckMode -->|&#34;非公平(默认)&#34;| NonFairImpl

    FairImpl -.->|&#34;新到达者检查队头是否互补&#34;| QHead
    NonFairImpl -.->|&#34;新到达者检查栈顶是否互补&#34;| STop

    QHead -.->|&#34;互补则配对交换&#34;| MatchedQ[&#34;配对成功<br/>分别唤醒&#34;]
    STop -.->|&#34;互补则配对交换&#34;| MatchedS[&#34;配对成功<br/>分别唤醒&#34;]

图 4-2 主旨概括:该图展示了 SynchronousQueue 公平和非公平两种配对路径。所有操作都汇聚到 transfer 方法,根据 fair 参数选择不同的数据结构。公平队列从队头匹配,非公平栈从栈顶匹配,匹配成功时双方线程交换数据并继续执行。

配对流程说明

  1. 线程调用 put(e)take(),进入 transfer 方法。
  2. 根据队列/栈的当前状态,判断是否存在互补模式的等待节点。
  3. 若存在互补节点:立即匹配成功。将数据从生产者传递给消费者,然后唤醒对方节点。
  4. 若不存在互补节点:当前线程将自己包装成节点,按照公平/非公平策略入队或压栈,然后通过 LockSupport.park 挂起,等待对端线程到来。

4.3.2 公平模式:TransferQueue (FIFO)

公平模式保证等待最久的线程优先被匹配。

数据结构TransferQueue 内部维护一个 QNode 的双向链表(队列)。head 指向队头,tail 指向队尾。新到达的线程节点总是追加到 tail,匹配时从 head 开始向后扫描,寻找第一个互补节点。

配对规则

  • 新到达的线程检查 head 是否不为空,且 head 节点的模式与自身互补。
  • 若互补,则将 head 节点出队,完成配对,交换数据并唤醒对方。
  • 若队列为空,或队头节点模式相同,则当前线程节点入队尾,挂起等待。

4.3.3 非公平模式:TransferStack (LIFO,默认)

非公平模式利用栈的后进先出特性,让最新到达的线程优先配对。

数据结构TransferStack 内部维护一个 SNode 的单链表栈。head 指向栈顶。节点包含 mode 字段,取值为 REQUEST(消费者)或 DATA(生产者)。

配对规则

  • 新到达的线程检查栈顶 head 节点。
  • headnull 或栈顶节点模式与自身相同,则当前节点压栈,挂起等待。
  • 若栈顶节点模式与自身互补,则尝试将栈顶节点弹出,并与之配对。配对成功后,交换数据,唤醒对方。
  • 栈顶优先匹配使得刚刚阻塞的线程很可能被立刻唤醒,利用了 CPU 缓存的热点,从而获得更高的吞吐量。但代价是等待最久的线程可能长时间得不到匹配(饥饿)。

Tip:公平与非公平同时对生产者和消费者起效

SynchronousQueue 中,公平与非公平策略同时对生产者和消费者生效,因为它们共用同一个数据结构。

  • 非公平模式(TransferStack):生产者节点和消费者节点混在同一个栈中。配对时总是从栈顶取节点,无论它是生产者还是消费者。这意味着最近到达的线程优先被匹配,遵循统一的 LIFO 规则。
  • 公平模式(TransferQueue):所有线程节点按到达顺序排在一个 FIFO 队列中。配对时从队头取节点,等待最久的线程(无论角色)优先被匹配

这与直觉中“生产者公平、消费者也公平”的理解一致——SynchronousQueue 的公平性针对的是线程的等待时长,而非数据的顺序。

Tip:与 ArrayBlockingQueue 公平锁的本质区别

SynchronousQueue 的公平配对与 ArrayBlockingQueue 的公平锁是两个不同维度的概念:

维度ArrayBlockingQueue 公平锁SynchronousQueue 公平配对
公平的是什么锁的获取顺序——等待锁最久的线程优先获得锁配对优先级——等待最久的线程优先被匹配
作用对象所有竞争同一把锁的线程混在同一个等待队列中的生产者与消费者
等待队列生产者等 notFull,消费者等 notEmpty分开的同一个队列/栈,不区分角色
出队/出栈顺序仍由底层数组 FIFO 保证公平模式 FIFO,非公平模式 LIFO

一句话总结ArrayBlockingQueue 的公平锁解决“谁先操作队列”;SynchronousQueue 的公平配对解决“谁先被匹配”。前者是对锁的调度,后者是对线程服务的调度。

4.3.4 多生产者多消费者场景分析

为了具体理解配对顺序,我们构建一个场景:三个生产者依次调用 put,然后三个消费者依次调用 take,分别观察公平与非公平模式下的配对结果。

非公平模式(默认)运行结果:消费者收到数据的顺序为 3→2→1,即最新阻塞的生产者最先被匹配。时序如下:

sequenceDiagram
    participant P1 as 生产者1
    participant P2 as 生产者2
    participant P3 as 生产者3
    participant TS as TransferStack
    participant C1 as 消费者1
    participant C2 as 消费者2
    participant C3 as 消费者3

    Note over P1,TS: 初始栈空

    P1->>TS: transfer(Data-1)
    Note over TS: 栈空,压入 DATA(P1),栈顶=P1
    TS-->>P1: park

    P2->>TS: transfer(Data-2)
    Note over TS: 栈顶 DATA,相同,压入 P2,栈顶=P2
    TS-->>P2: park

    P3->>TS: transfer(Data-3)
    Note over TS: 栈顶 DATA,相同,压入 P3,栈顶=P3
    TS-->>P3: park

    Note over P1,TS: 栈顶是 P3,栈底是 P1

    C1->>TS: transfer(null)
    Note over TS: 栈顶 DATA(P3) 互补,弹出配对
    TS-->>C1: 返回 Data-3
    TS->>P3: unpark

    C2->>TS: transfer(null)
    Note over TS: 栈顶 DATA(P2) 互补,弹出配对
    TS-->>C2: 返回 Data-2
    TS->>P2: unpark

    C3->>TS: transfer(null)
    Note over TS: 栈顶 DATA(P1) 互补,弹出配对
    TS-->>C3: 返回 Data-1
    TS->>P1: unpark

图 4-3 主旨概括:非公平模式(栈)下,后到达的生产者位于栈顶,先被消费者匹配,因此消费者得到数据的顺序是 3→2→1。最近阻塞的线程优先被唤醒,有利于利用 CPU 缓存热点,提升吞吐量,但可能导致最早等待的线程饥饿。

公平模式运行结果:消费者收到数据的顺序为 1→2→3,严格按到达顺序配对。时序如下:

sequenceDiagram
    participant P1 as 生产者1
    participant P2 as 生产者2
    participant P3 as 生产者3
    participant TQ as TransferQueue
    participant C1 as 消费者1
    participant C2 as 消费者2
    participant C3 as 消费者3

    Note over P1,TQ: 初始队列空

    P1->>TQ: transfer(Data-1)
    Note over TQ: 队列空,入队,队头=P1
    TQ-->>P1: park

    P2->>TQ: transfer(Data-2)
    Note over TQ: 队尾为 P1,模式相同,入队
    TQ-->>P2: park

    P3->>TQ: transfer(Data-3)
    Note over TQ: 入队
    TQ-->>P3: park

    Note over P1,TQ: 队头 P1,队尾 P3

    C1->>TQ: transfer(null)
    Note over TQ: 队头 DATA(P1) 互补,出队配对
    TQ-->>C1: 返回 Data-1
    TQ->>P1: unpark

    C2->>TQ: transfer(null)
    Note over TQ: 队头 DATA(P2) 互补,出队配对
    TQ-->>C2: 返回 Data-2
    TQ->>P2: unpark

    C3->>TQ: transfer(null)
    Note over TQ: 队头 DATA(P3) 互补,出队配对
    TQ-->>C3: 返回 Data-3
    TQ->>P3: unpark

图 4-4 主旨概括:公平模式(队列)下,先到达的生产者位于队头,被消费者优先匹配,消费者得到数据的顺序严格为 1→2→3。保证了等待最久的线程优先服务,避免饥饿。

Tip:为什么生产者和消费者共用同一个栈/队列?

这正是 SynchronousQueue “零容量”语义的集中体现:

  • 队列本身不存储数据,只维护等待配对的线程节点
  • 如果生产者和消费者各用各的栈,配对时需要跨结构查找互补节点,复杂度大增,也不符合“一手交钱,一手交货”的直接传递模型。
  • 共用同一个栈/队列,让配对逻辑简洁自然:来者不拒,见栈顶/队头,互补则匹配,相同则等待

这也解释了为什么非公平模式下,既可能出现“生产者堆积”,也可能出现“消费者堆积”,完全取决于到达顺序。

4.4 核心 transfer 方法与通知机制

4.4.1 transfer 方法实现精要

transfer 方法是一个集自旋、挂起、超时、取消于一体的复杂逻辑。以非公平 TransferStack 为例,简化流程如下:

  1. 自旋尝试配对:进入主循环,读取栈顶 h = head
  2. 栈为空或模式相同:当前线程需要等待。将自身包装为 SNode,尝试通过 CAS 将其压入栈顶。成功后,通过 LockSupport.park 挂起。被唤醒后继续自旋。
  3. 模式互补:尝试 CAS 弹出栈顶节点。成功则配对,将数据写入对方节点的 match 字段,并 LockSupport.unpark 对方。返回对方传回的值。
  4. 超时/中断处理:等待过程中若超时或被中断,尝试将自身节点从栈中取消,并唤醒后继节点帮助清理。

4.4.2 put/take 源码

由于 SynchronousQueue 不暴露 enqueue/dequeue,我们直接看 puttake 源码:

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    if (transferer.transfer(e, false, 0) == null) {
        Thread.interrupted();
        throw new InterruptedException();
    }
}

public E take() throws InterruptedException {
    E e = transferer.transfer(null, false, 0);
    if (e != null)
        return e;
    Thread.interrupted();
    throw new InterruptedException();
}

offerpoll 的超时版本则传入 timed=true 和超时纳秒数,transfer 内部会在等待时使用 parkNanos,超时后返回特定值。

4.4.3 通知机制:LockSupport.park/unpark 的无锁直接唤醒

SynchronousQueue 的通知机制完全不同于 ArrayBlockingQueueLinkedBlockingQueue 基于 ReentrantLock + Conditionawait/signal 模型。它使用的是 LockSupport.park/unpark 实现的无锁、点对点直接唤醒

核心要素:每个等待配对的线程节点(SNodeQNode)内部保存了当前线程的引用:

volatile Thread waiter;   // 等待的线程引用

unpark 的触发流程:以非公平模式生产者先到、消费者后到为例:

  • 生产者创建 SNode,设置 waiter = Thread.currentThread(),CAS 压栈,然后 LockSupport.park(this) 挂起。
  • 消费者到达,发现栈顶互补,CAS 弹出该节点,读取 waiter,调用 LockSupport.unpark(waiter) 直接唤醒生产者。
  • 生产者从 park() 返回,检查节点的 match 字段,确认配对成功,继续执行。
sequenceDiagram
    participant P as 生产者线程
    participant N as SNode(生产者)
    participant TS as TransferStack
    participant C as 消费者线程

    P->>N: 创建 SNode(mode=DATA, waiter=P, item=e)
    P->>TS: CAS 压入栈顶
    Note over TS: head → SNode(DATA)
    P->>P: LockSupport.park() 挂起

    C->>TS: transfer(null) 检查栈顶
    Note over TS: head.mode == DATA,互补!
    C->>TS: CAS 弹出栈顶 SNode
    TS-->>C: 返回生产者节点 m
    C->>N: 读取 m.item = e
    C->>N: 设置 m.match = C的节点
    C->>P: LockSupport.unpark(P) ★ 唤醒生产者
    C->>C: 从 transfer 返回 e

    P->>P: 从 park() 返回
    P->>N: 检查 s.match != null → 配对成功
    P->>P: 从 transfer 返回 (完成)

图 4-5 主旨概括:此时序图展示了 SynchronousQueue 基于 LockSupport.park/unpark 的无锁通知机制。配对线程通过 CAS 弹出节点后,直接读取节点中的 waiter 引用,调用 unpark 精确唤醒对方,无需经过 Condition 队列。

Tip:为什么 Condition 不采用 LockSupport.park/unpark 直接唤醒,而要经过 AQS 队列中转?

(1)语义复杂度不同
SynchronousQueue 的等待目标极简:一个线程只等一个对端线程,配对完成后双方不再检查任何共享状态。
Condition 则复杂得多:线程等待的是一个布尔谓词成真(如 count > 0),该谓词可能被多个线程竞争。被唤醒后必须重新竞争锁、重新检查条件、失败则再次 await。直接 unpark 后让线程返回将无法安全验证条件是否真正满足。

(2)锁的语义不同
Condition.await() 的核心契约是原子地释放锁并挂起线程,这必须由 AQS 同步队列和条件队列配合完成,以保证释放锁与入队之间的原子性,避免信号丢失。SynchronousQueue 没有锁,无需此约束。

(3)通用性不同
Condition 需要支持多个条件变量、signalAll、超时、中断等通用语义,而 SynchronousQueue 只是一个高度特化的组件,为极致场景做极致优化。两者定位不同,一个追求通用正确,一个追求专用高效。

Tip:SynchronousQueue 实际上只是一个生产消费的阻塞会合点

从更高的抽象看,SynchronousQueue 彻底抛弃了传统队列“存储中转”的职能,将“队列”推至极端——没有缓冲区,没有容量,只有一个让线程相遇、交换数据然后各自离开的交汇点。它的内部结构(TransferStack/TransferQueue)存储的是等待配对的线程节点,而不是数据元素。因此,它本质上是一个纯粹的生产者-消费者阻塞会合点

4.5 优点与局限性

优点

  • 零内存开销:不存储元素,没有数组或链表的空间消耗,对 GC 压力极小。
  • 极低延迟:数据直接从生产者到消费者,减少了拷贝和排队时间。
  • 天然背压:无缓冲意味着系统负载会实时反馈给生产者,防止任务堆积。
  • 可选的公平性:适应不同场景对吞吐和公平的取舍。

局限性

  • 速率严格匹配要求:如果生产者和消费者速率不匹配,任一方将频繁阻塞,系统吞吐可能剧烈下降。
  • 无缓冲导致线程频繁挂起:在高并发但任务执行极短的场景下,线程的挂起/唤醒开销可能成为瓶颈。
  • 调试困难:由于无内部状态,peeksize 等方法永远返回空值,无法像其他队列那样窥探内部元素来进行监控。
  • 饥饿风险(非公平模式):栈模式可能导致先到达的生产者始终得不到匹配。

4.6 典型应用深度:与线程池的配合

ThreadPoolExecutor 使用 SynchronousQueue 并搭配 CallerRunsPolicy 是经典的“无界线程增长 + 背压”模式:

  • 新任务提交时,如果线程数未达 maximumPoolSize,则会立即创建新线程执行。
  • 如果所有线程都在忙碌且线程数已达上限,offer(task) 立刻失败,触发 CallerRunsPolicy,任务被回退给提交任务的线程(如主线程)执行。
  • 这导致主线程在任务执行完之前无法提交下一个任务,从而自然地减缓了任务生产速率,实现了背压

这种组合使得系统能够动态扩缩线程,同时在极限压力下阻止任务无限堆积,是构建弹性系统的利器。


5. PriorityBlockingQueue:无界世界里的堆排序

PriorityBlockingQueueBlockingQueue 家族中第一个完全放弃 FIFO 顺序的实现。它以二叉小顶堆为底层结构,赋予队列“优先级”语义——每次出队总能拿到当前队列中优先级最高的元素。同时,由于采用了无界设计,生产者永远不会因为队列满而阻塞。本章将从数据结构基础、底层实现、阻塞模型、核心操作及工程应用等维度,全面拆解 PriorityBlockingQueue 的内部运作。

5.1 定位、特性与适用场景

定位PriorityBlockingQueue无界、可扩容的优先级阻塞队列。它以空间换语义,牺牲了 FIFO 的顺序性,换取了按优先级出队的能力,同时通过无界设计消除了生产者的阻塞风险。

核心特性

  • 二叉小顶堆:底层为 Object[] 数组实现的完全二叉树,queue[0] 始终是优先级最高的元素(最小值)。
  • 无界自动扩容:队列没有容量上限。入队时若空间不足,会自动扩容(tryGrow),因此 put 方法永远不会阻塞。
  • 单锁并发:全局唯一 ReentrantLock lock 保护所有操作,但扩容通过 CAS 自旋锁 allocationSpinLock 控制,允许多个线程协助扩容。
  • 单一条件变量:只有 notEmpty 条件(无 notFull),消费者在队列空时阻塞等待,生产者永不阻塞。
  • 排序支持:元素必须实现 Comparable,或在构造时传入 Comparator。堆中所有父子节点都满足比较器/自然顺序的小顶堆关系。
  • 迭代无序:迭代器返回的是底层数组的物理顺序,不保证任何优先级顺序。只有通过 take/poll 才能获得优先级顺序。

适用场景

  • VIP 任务调度:按优先级处理请求、消息、订单等,确保高优先级任务先执行。
  • 游戏 AI 更新:按优先级(如距离、重要性)排序的实体列表,每次取出最优先的一个处理。
  • 资源分配:按优先级分配计算资源、带宽等。
  • 需要无界缓冲但又有优先级要求的线程池任务队列(注意内存风险)。

不适用场景

  • 严格 FIFO 要求:应使用 ArrayBlockingQueueLinkedBlockingQueue
  • 需要延迟/定时功能:应使用 DelayQueue
  • 内存敏感且任务量巨大:无界可能引发 OOM,需配合监控或改用有界队列。

5.2 数据结构基础:二叉堆

在深入源码之前,必须先理解 PriorityBlockingQueue 的灵魂——二叉堆。它既是一种逻辑结构(完全二叉树),也是一种物理实现(数组)。

5.2.1 堆的定义与性质

在数据结构领域,“堆”特指一种满足堆性质的完全二叉树:

  • 结构性质:堆是一棵完全二叉树。除了最后一层外,其余所有层都是满的,且最后一层的节点尽可能靠左排列。这一性质使得堆可以无缝地映射到一个一维数组中,而没有任何空洞。
  • 堆序性质:对于小顶堆(Min-Heap),任意父节点的值 ≤ 其子节点的值,因此堆顶(根节点)是整个堆的最小值。对于大顶堆(Max-Heap),任意父节点的值 ≥ 其子节点的值,堆顶为最大值。

PriorityBlockingQueue 内部使用的正是小顶堆queue[0] 始终是优先级最高(数值最小)的元素。注意,堆只保证父子之间的偏序关系,并不保证兄弟节点之间的大小顺序,更不保证整个数组的完全有序。

5.2.2 数组存储:完全二叉树的极致映射

为什么堆不像普通二叉树那样用节点对象(含 left/right 指针)存储?答案在于完全二叉树的数学性质:

对于数组中索引为 i 的节点:

  • 父节点索引:(i - 1) >>> 1
  • 左子节点索引:(i << 1) + 1
  • 右子节点索引:(i << 1) + 2

由于树是完全的,数组中从 0size-1 的每一个位置都恰好对应一个有效节点,没有任何浪费。这意味着:

  • 零指针开销:不需要存储任何 left/right 引用,内存占用极低。
  • 极致缓存友好:所有元素在内存中连续存放,CPU 缓存行可以一次性加载多个父子节点,siftUpsiftDown 操作中的数据几乎总是在缓存中,性能极高。
  • 分配成本低:只需一个数组对象的分配,后续扩容时仅需数组拷贝,无需为每个元素分配独立节点对象。

相比之下,若使用树节点(如 Node { E item; Node left; Node right; }),每个元素都会有对象头、引用字段的开销,且节点在堆内存中离散分布,缓存缺失频繁,性能远不如数组实现的堆。

5.2.3 偏序 vs 全序:为什么堆比红黑树更适合优先级队列?

维度二叉堆(Binary Heap)红黑树(Red-Black Tree)
维护的序偏序:只保证父 ≤ 子,兄弟无序全序:左子树 < 根 < 右子树
取最小值O(1):直接取 queue[0]O(log n):沿左子树下行
插入O(log n),仅需单路径上浮O(log n),需旋转和颜色调整
删除O(log n),仅需单路径下沉O(log n),需旋转和颜色调整
内存占用极低(仅数组 + 引用)高(每个节点有 left/right/parent/color)
缓存局部性极好(连续内存)差(节点离散分布)
有序遍历不支持支持(中序遍历)

优先级队列只需要“每次取最小”,并不需要“所有元素按序排列”。堆以 O(1) 取最小值、极低的常数开销和优秀的内存特性,完美胜出。全序树对于优先级队列而言是功能过剩且性能更低的选择。

5.3 底层数据结构源码:数组如何成为堆

public class PriorityBlockingQueue<E> extends AbstractQueue<E>
    implements BlockingQueue<E>, java.io.Serializable {

    private static final int DEFAULT_INITIAL_CAPACITY = 11;
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    private transient Object[] queue;          // 核心存储
    private transient int size;                // 元素个数
    private transient Comparator<? super E> comparator; // 比较器
    private final ReentrantLock lock;          // 全局锁
    private final Condition notEmpty;          // 非空条件
    private transient volatile int allocationSpinLock; // 扩容自旋锁
}
  • queue:数组从索引 0 开始使用,queue[0] 是堆顶,queue[size-1] 是最后一个元素。
  • size:有效元素数量。
  • comparator:若为 null,元素必须实现 Comparable
  • locknotEmpty:保证线程安全,消费者在空时阻塞。
  • allocationSpinLock:CAS 自旋锁,用于扩容时的并发控制。

数组到树的映射关系

         queue[0]          ← 堆顶(最小)
        /        \
   queue[1]    queue[2]
    /    \       /    \
queue[3] queue[4] ...
  • 索引 i 的父节点:(i - 1) >>> 1
  • 左孩子:(i << 1) + 1
  • 右孩子:(i << 1) + 2
  • 最后一个非叶子节点索引:(size >>> 1) - 1

5.4 阻塞模型:单锁 + notEmpty 的无界等待

PriorityBlockingQueue 是无界队列,生产者永远不会因队列满而阻塞,因此其阻塞模型比有界队列简单很多。

flowchart LR
    subgraph Monitor[&#34;单锁管程&#34;]
        L[&#34;ReentrantLock<br/>lock&#34;]
        NE[&#34;notEmpty<br/>Condition<br/>(队列空时等待)&#34;]
        L --- NE
    end
    Producer[&#34;生产者&#34;] -->|&#34;lock → 必要时扩容 → siftUp → signal&#34;| L
    Consumer[&#34;消费者&#34;] -->|&#34;lock → while dequeue==null → await&#34;| NE
    NE -.->|&#34;signal 唤醒&#34;| Consumer

图 5-1 主旨概括:此图展示了 PriorityBlockingQueue 的简单阻塞模型:单锁保护,无 notFull,消费者仅在队列空时阻塞。生产者入队后若发现队列原为空,则唤醒消费者。

设计要点

  • 生产者永不阻塞put 直接调用 offer,内部仅检查扩容需求,然后通过 siftUp 插入元素。若插入前队列为空(size == 0),则调用 notEmpty.signal() 唤醒消费者。
  • 消费者空时阻塞takewhile 循环中调用 dequeue()。若返回 null(队列空),则 notEmpty.await() 释放锁并阻塞;被唤醒后重新竞争锁,再次尝试出队。
  • 超时版本poll(time, unit) 使用 notEmpty.awaitNanos(…) 实现限时等待。

Tip:单锁控制下,吞吐量受限但已有优化

由于底层是堆结构,入队的 siftUp 和出队的 siftDown 都需要沿父子路径比较和交换,路径可能在树的任意层级交叉,因此无法像链表那样通过锁分离实现入队出队并行。必须全局互斥。

但 Doug Lea 做了一个关键优化:扩容时释放锁(tryGrow 中先 lock.unlock(),CAS 获取 allocationSpinLock,完成扩容后再 lock.lock()),允许在扩容期间出队操作继续执行,减少了停顿时间。这使得 PriorityBlockingQueue 的吞吐量在优先级队列中已经做到极致。

5.5 核心操作:siftUp、siftDown 与 put/take 源码

5.5.1 入队:siftUp 上浮

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;
}
  • k 为新元素插入的初始位置(数组末尾 size)。
  • 不断与父节点比较,若新元素更小则交换,直到到达合适位置。
  • 时间复杂度 O(log n)。

5.5.2 出队:siftDown 下沉

private static <T> void siftDownComparable(int k, T x, Object[] array, int n) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    int half = n >>> 1;
    while (k < half) {
        int child = (k << 1) + 1;
        Object c = array[child];
        int right = child + 1;
        if (right < n && ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
            c = array[child = right];
        if (key.compareTo((T) c) <= 0)
            break;
        array[k] = c;
        k = child;
    }
    array[k] = key;
}
  • 出队时取走 queue[0],将数组末尾元素放到堆顶,然后执行 siftDown 下沉。
  • 不断与左右子节点中较小的比较,若自身更大则交换,直到堆性质恢复。
  • 时间复杂度 O(log n)。

5.5.3 put 与 take 源码

public void put(E e) {
    offer(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;
        if (n == 0)
            notEmpty.signal();
    } finally {
        lock.unlock();
    }
    return true;
}

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

private E dequeue() {
    int n = size;
    if (n == 0) return null;
    Object[] array = queue;
    E result = (E) array[0];
    E x = (E) array[--n];
    array[n] = null;
    if (n != 0)
        siftDown(0, x, array, n);
    size = n;
    return result;
}

时序图(生产者先入队,消费者后到达)

sequenceDiagram
    participant P as 生产者
    participant L as lock
    participant Q as queue(堆)
    participant NE as notEmpty
    participant C as 消费者

    P->>L: lock()
    P->>Q: 检查容量,必要时扩容
    P->>Q: siftUp 将元素插入堆中
    P->>NE: signal() 唤醒消费者
    P->>L: unlock()

    C->>L: lock()
    C->>Q: dequeue() 取堆顶
    Q-->>C: 返回最小元素
    C->>L: unlock()

图 5-2 主旨概括:此时序图展示了生产者先入队,消费者后出队的完整流程。生产者加锁后完成扩容和堆插入,若原队列为空则唤醒消费者;消费者加锁后从堆顶取走最小元素。

5.6 扩容机制:释放锁的并发优化

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));
            if (newCap - MAX_ARRAY_SIZE > 0)
                newCap = hugeCapacity(oldCap);
            newArray = new Object[newCap];
        } finally {
            allocationSpinLock = 0;
        }
    }
    if (newArray == null)
        Thread.yield();
    lock.lock();            // ★ 重新获取主锁
    if (newArray != null && queue == array) {
        queue = newArray;
        System.arraycopy(array, 0, newArray, 0, oldCap);
    }
}

设计要点

  • 扩容前主动 lock.unlock(),使得出队操作(take/poll)在扩容期间可以继续执行,不会被阻塞。
  • 通过 CAS 自旋锁 allocationSpinLock 保证只有一个线程进行内存分配,其余线程 Thread.yield() 等待。
  • 扩容完成后重新 lock(),拷贝数据,替换引用。

这个优化使得 PriorityBlockingQueue 在高并发且频繁扩容的场景下,仍能保持较好的响应性。

5.7 优点、局限性与工程考量

优点

  • 精确的优先级语义:每次 take 都返回优先级最高的元素。
  • 无界、无生产者阻塞:适合无法预知任务量的调度系统。
  • 内存紧凑:数组存储,无节点指针开销,缓存友好。
  • 扩容友好:释放锁扩容,减小停顿时间。

局限性

  • 单锁互斥:入队与出队无法并行,高并发下吞吐量受限(根源在于堆的全局调整特性,无法做锁分离)。
  • 无界可能 OOM:必须配合监控和限流使用。
  • 低优先级饥饿:源源不断的高优先级元素会让低优先级永远得不到执行,需要业务层自行设计“老化”或“公平插队”策略。
  • 迭代无序:迭代器只能遍历数组物理顺序,不反映优先级。

Tip:为什么堆不能像链表那样锁分离?

链表的入队(操作 last)和出队(操作 head)在物理上完全隔离,因此可以拆成两把锁。而堆的 siftUpsiftDown 操作的是同一棵树的节点,二者都需要沿着父-子路径向上或向下比较和交换,这些路径可能在树的任意层级交叉。出队操作还会将堆尾元素移到堆顶,这意味着出队不仅读堆顶,还会修改堆中任意位置。如果入队同时在不同锁下进行,两个操作可能同时修改同一个父-子区域,导致堆性质被破坏。因此,堆操作必须是全局互斥的。

Tip:偏序(堆)比全序(如红黑树)更快的原因

在优先级队列这种“只取最值”的场景下,二叉堆的偏序维护比全序结构快得多:

  • 取最小值 O(1) vs O(log n):堆顶直接取,全序结构需沿最左侧下行。
  • 插入/删除常数因子小:堆的 siftUp/siftDown 在数组上完成,仅涉及父子比较和交换,无旋转和颜色调整。CPU 缓存友好。
  • 内存效率高:数组 vs 对象节点,百万级元素内存节约巨大,GC 压力小。 全序对于优先级队列而言是功能过剩且性能更低的选择。