1. 概述
在 Java 并发编程的宏大体系中,java.util.concurrent.BlockingQueue 及其子接口、实现类扮演着线程间协调与通信的枢纽 角色。它不仅仅是存放对象的容器,更是一套融合了同步策略、内存可见性保证、线程挂起与唤醒机制的精密工具。
核心特性的底层意义:
-
线程安全(Thread Safety) : 阻塞队列通过内部锁(
ReentrantLock)或 CAS(Compare-And-Swap)无锁算法,保证了多线程同时调用put、take、offer、poll等方法的原子性 与可见性 。例如,ArrayBlockingQueue使用单把ReentrantLock守护整个数组,保证了count、putIndex、takeIndex三个状态变量的修改对其他线程立即可见(遵循happens-before原则)。 -
阻塞操作(Blocking Operations) :
put(e)与take()的阻塞并非简单的while循环加Thread.sleep(),而是依托LockSupport.park()或Condition.await()将线程精准地移出 CPU 调度队列,直到条件满足时才被唤醒。这避免了无谓的 CPU 空转(Busy Spin),是构建高效生产者-消费者模型的核心。 -
容量控制(Capacity Control): 阻塞队列提供了**背压(Backpressure)**机制。当生产者速率远超消费者时,有界队列通过强制生产者等待(阻塞),将压力反馈给上游系统,防止下游消费者因内存耗尽而崩溃。
本文的深度目的: 不仅罗列 API 差异,更深入 JVM 内存布局、锁竞争对 CPU 流水线的影响、GC 压力与吞吐量的平衡 。我们将像庖丁解牛一般,剖析 SynchronousQueue 的栈与队列公平策略,解读 LinkedTransferQueue 的松弛型双重队列算法,并对比 ArrayBlockingQueue 与 LinkedBlockingQueue 在伪共享(False Sharing)方面的不同表现。最终帮助读者在架构设计时,能像使用手术刀一样精准地选择合适的队列。
2. 阻塞队列体系概览(深度扩展)
2.1 增强型类图(Mermaid)
增加了接口继承层次中的 TransferQueue 接口(JDK 7 引入),它是对 BlockingQueue 的重要语义补充。
2.2 队列深度介绍表
| 名称 | 接口 | 数据结构详解 | 有界性严格定义 | 锁/同步机制核心类 |
|---|---|---|---|---|
| ArrayBlockingQueue | BlockingQueue |
循环数组 。利用 putIndex 和 takeIndex 指针绕回,避免元素搬移。内存连续,对 CPU 缓存行(Cache Line)友好。 |
强有界。构造后容量不可变,满时入队阻塞,空时出队阻塞。 | ReentrantLock(可指定公平/非公平)+ Condition(notEmpty/notFull)。 |
| LinkedBlockingQueue | BlockingQueue |
单向链表 。头结点 head 通常为哑元(Dummy Node),尾节点 last 指向最后一个有效元素。节点对象分散在堆内存中。 |
可选有界 。若构造传入容量,则在达到容量时阻塞 put;若不传,容量为 Integer.MAX_VALUE,此时 put 永不阻塞(仅在资源耗尽时 OOM)。 |
双锁双条件 :putLock 保护尾部和 count 增加,takeLock 保护头部和 count 减少。AtomicInteger count 规避双锁间的数据竞争。 |
| PriorityBlockingQueue | BlockingQueue |
二叉堆数组 。queue[n] 的子节点在 2n+1 和 2n+2。入队自下而上堆化(siftUp),出队自上而下堆化(siftDown)。 |
无界 。初始容量默认 11,自动扩容(tryGrow),上限受限于 Integer.MAX_VALUE - 8(数组最大长度)。 |
单锁 ReentrantLock + notEmpty Condition。扩容时通过 allocationSpinLock 自旋锁短暂释放主锁,减少阻塞。 |
| DelayQueue | BlockingQueue |
包装 PriorityQueue 。内部 PriorityQueue<Delayed> 按剩余延时排序。 |
无界 。同 PriorityQueue。 |
单锁 ReentrantLock + available Condition。采用 Leader-Follower 线程模型 最小化唤醒竞争。 |
| SynchronousQueue | BlockingQueue |
无存储结构 。公平模式下为 FIFO 队列(TransferQueue),非公平模式下为 LIFO 栈(TransferStack)。节点表示数据或等待者。 |
容量严格为 0 。任何 size()/remainingCapacity() 返回 0。 |
CAS + LockSupport.park/unpark。完全无锁化匹配。 |
| LinkedTransferQueue | TransferQueue |
松弛型双重队列(Slack Dual Queue) 。节点包含 isData 标志。数据节点(生产者)和请求节点(消费者)在同一链表中并存,匹配时互相消除。 |
无界。通过 CAS 维护链表结构。 | CAS + LockSupport.park/unpark 。xfer 方法处理 NOW、ASYNC、SYNC、TIMED 四种模式。 |
| LinkedBlockingDeque | BlockingDeque |
双向链表 。节点包含 prev 和 next 指针。支持头部和尾部独立操作。 |
可选有界 。同 LinkedBlockingQueue。 |
单锁 ReentrantLock + notEmpty/notFull。由于双向指针修改的原子性要求,无法拆分为双锁。 |
| ConcurrentLinkedQueue | Queue |
非阻塞单向链表。 | 无界 | 纯 CAS 自旋(非阻塞)。 |
3. 核心维度对比(深度多维度表格)
维度一:数据结构与内存存储(扩展:CPU 缓存友好性)
| 队列 | 数据结构 | 内存分布 | CPU 缓存命中率 | 伪共享风险 |
|---|---|---|---|---|
| ArrayBlockingQueue | 数组 | 连续内存块(Eden 或 Old Gen) | 极高。入队出队仅移动索引,元素紧密相邻,预取高效。 | 存在 。putIndex 和 takeIndex 可能在同一缓存行,导致修改时互相失效。JDK 通过 @Contended 或手动填充避免。 |
| LinkedBlockingQueue | 单向链表 | 堆内存随机分布(Node 对象地址不连续) | 较低 。每次访问 node.next 可能触发缓存缺失(Cache Miss)。 |
节点间独立,无伪共享。 |
| PriorityBlockingQueue | 数组堆 | 连续内存块(但扩容拷贝时内存地址改变) | 较高。堆顶元素访问频繁,热点数据集中在数组头部。 | 同 ArrayBlockingQueue。 |
| DelayQueue | PriorityQueue | 同 PriorityBlockingQueue。 |
较高。 | 同 ArrayBlockingQueue。 |
| SynchronousQueue | 节点栈/队列 | 仅在匹配期间存在,通常位于年轻代(快速回收)。 | 无关紧要。因为数据几乎不存储,直接在线程栈/寄存器间传递。 | 无。 |
| LinkedTransferQueue | 松弛节点链表 | 堆内存随机分布。 | 中等。由于松弛设计,匹配后可快速移除节点。 | 无。 |
| LinkedBlockingDeque | 双向链表 | 堆内存随机分布。 | 较低。双端操作可能访问链表中间,缓存不友好。 | 无。 |
维度二:有界性(扩展:内存溢出风险分析)
| 队列 | 有界性 | 无界场景下 OOM 风险 | 如何控制风险 |
|---|---|---|---|
| ArrayBlockingQueue | 固定 | 无风险。构造即分配内存,超过容量即阻塞。 | 设置合理容量,配合 offer 超时机制。 |
| LinkedBlockingQueue | 可选 | 极高。默认无界,生产者持续写入时,Node 对象填满堆内存。 | 强制显式传入 capacity 参数。 |
| PriorityBlockingQueue | 无界 | 较高。堆元素本身占用内存,且数组频繁扩容产生垃圾。 | 业务层限流,或监控队列 size()。 |
| DelayQueue | 无界 | 极高。延时元素若未被及时消费,会持续堆积。 | 监控过期时间,设置最大容量限制(需自定义子类)。 |
| SynchronousQueue | 容量 0 | 无内存风险。 | 无。 |
| LinkedTransferQueue | 无界 | 较高。链表节点持续增长。 | 同上。 |
| LinkedBlockingDeque | 可选 | 极高。双端节点内存开销大于单端链表。 | 必须显式指定容量。 |
维度三:锁与并发机制(扩展:AQS 实现细节)
| 队列 | 锁机制 | AQS 使用细节 | 自旋优化 |
|---|---|---|---|
| ArrayBlockingQueue | 单锁 | 直接使用 ReentrantLock(内部 Sync 继承 AQS)。notFull.await() 释放锁并进入条件队列。 |
无显式自旋,依赖 ReentrantLock 内部的短时自旋优化。 |
| LinkedBlockingQueue | 双锁 | 两把独立的 ReentrantLock。signalNotFull 时可能需要获取 putLock 后再获取 takeLock 进行级联通知。 |
同上。 |
| PriorityBlockingQueue | 单锁 + 扩容自旋 | 扩容时使用 allocationSpinLock(一个 volatile int 标志)配合 Thread.yield() 让出 CPU,避免长时间持锁阻塞读写。 |
扩容期间的自旋等待。 |
| DelayQueue | 单锁 + Leader-Follower | available.awaitNanos(delay) 精准休眠。Leader 线程负责等待,Follower 线程负责消费。 |
等待前通常会有短时间自旋检查延时状态。 |
| SynchronousQueue | 无锁 | 不使用 AQS。直接基于 LockSupport 和 CAS 构建匹配器。 |
重度依赖自旋 。匹配前会自旋尝试若干次(默认 512 次),失败后才 park。 |
| LinkedTransferQueue | 无锁 | 不使用 AQS。复杂的状态机(Node 的 item、waiter、next 字段通过 CAS 流转)。 |
智能自旋 。awaitMatch 方法中根据前驱节点状态决定自旋次数。 |
| LinkedBlockingDeque | 单锁 | 同 ArrayBlockingQueue。 |
无显式自旋。 |
维度四:阻塞行为(扩展:线程状态转换)
| 队列 | 阻塞发生条件 | 线程状态流转 |
|---|---|---|
| ArrayBlockingQueue | 队列满 put,队列空 take。 |
RUNNABLE -> WAITING (parked in Condition.await()) -> BLOCKED (竞争锁) -> RUNNABLE。 |
| LinkedBlockingQueue | 有界且满 put,空 take。 |
同上。 |
| PriorityBlockingQueue | 仅队列空时 take 阻塞。 |
RUNNABLE -> WAITING -> ... |
| DelayQueue | 队列空 或 堆顶元素未过期。 | RUNNABLE -> TIMED_WAITING (awaitNanos) / WAITING -> ... |
| SynchronousQueue | 永远阻塞(除非配对线程出现)。 | RUNNABLE -> WAITING (parked by LockSupport.park) -> RUNNABLE。无锁状态直接挂起。 |
| LinkedTransferQueue | transfer 无配对者时阻塞;take 无数据且队列中无等待生产者时阻塞。 |
同上。 |
| LinkedBlockingDeque | 两端满/空时对应操作阻塞。 | 同 ArrayBlockingQueue。 |
维度五:公平性支持(扩展:源码实现路径)
| 队列 | 公平性参数 | 实现机制源码路径 |
|---|---|---|
| ArrayBlockingQueue | boolean fair |
ReentrantLock lock = new ReentrantLock(fair); 直接传递给 AQS。公平模式通过 hasQueuedPredecessors 检查等待队列。 |
| SynchronousQueue | boolean fair |
fair ? new TransferQueue<E>() : new TransferStack<E>()。TransferQueue 内部是 FIFO 单向链表,TransferStack 是 LIFO 栈。 |
| LinkedTransferQueue | 无参数,近似 FIFO | 松弛型双重队列:数据节点和请求节点分别按到达顺序排队,但匹配时允许"互补消除",整体上保持 FIFO 公平性。 |
4. 核心机制深度对比分析(源码级扩展)
4.1 锁与并发度:单锁 vs 双锁 vs 无锁的深度辨析
ArrayBlockingQueue 单锁困境:伪共享与吞吐量瓶颈
- 源码分析 :
put和take共享final ReentrantLock lock。这意味着即使数组有 1000 个空位,一个生产者依然会因为一个消费者正在调用take()而被阻塞在lock.lockInterruptibly()处。 - 性能影响 :在 CPU 密集型 的多核服务器上,单锁会导致严重的 上下文切换。每次锁释放,内核需要唤醒等待队列中的线程,引发用户态到内核态的切换,成本约 3-5 微秒。
- 缓存行失效 :
putIndex和takeIndex经常被同时修改。在单锁下这不是问题(因为互斥),但如果试图改造为无锁,这两个变量将产生严重的伪共享。
LinkedBlockingQueue 双锁的精妙之处:cascading notifies
-
源码分析 :
java// 简化的 put 逻辑 putLock.lockInterruptibly(); try { while (count.get() == capacity) notFull.await(); enqueue(node); c = count.getAndIncrement(); if (c + 1 < capacity) notFull.signal(); // 级联通知其他生产者 } finally { putLock.unlock(); } if (c == 0) signalNotEmpty(); // 此时队列从空变为非空,需要唤醒消费者关键点 :
signalNotEmpty()需要获取takeLock。但此时putLock已经释放,因此不会死锁。这就是双锁能够并行的核心原因------锁的获取顺序不再固定。
LinkedBlockingDeque 为何无法双锁?------双向指针的原子性噩梦
- 场景推演 :假设有
putLock保护尾部,takeLock保护头部。队列中只剩 1 个元素(head.next == tail)。此时生产者调用putFirst(头部入队),消费者调用takeLast(尾部出队)。putFirst需要修改head.next.prev指向新节点。takeLast需要修改tail.prev.next指向null。- 这两个操作修改的是同一个节点(原尾节点的前驱,即头节点)。如果使用两把不同的锁,这两个操作将并发执行,导致链表指针断裂。为了修正这一点需要引入复杂的双重 CAS 或全局协调机制,JDK 作者权衡后选择了简洁的单锁实现。
4.2 阻塞与唤醒:Leader-Follower 模式在 DelayQueue 中的精妙应用
-
问题背景 :假设有 10 个线程调用
DelayQueue.take(),堆顶元素还需 5 秒过期。如果所有线程都调用available.awaitNanos(5, SECONDS),5 秒后内核会唤醒所有 10 个线程,它们将疯狂竞争锁(Thundering Herd 惊群效应),最终只有一个线程能取走元素,其余 9 个线程发现没数据后又回去睡觉。这浪费了大量 CPU 和上下文切换。 -
Leader-Follower 解法(源码逻辑) :
java// DelayQueue.take() 伪代码 for (;;) { first = q.peek(); if (first == null) { available.await(); // 没数据,无限等 } else { delay = first.getDelay(NANOSECONDS); if (delay <= 0) { return q.poll(); // 数据过期,取走 } // 关键:释放 first 引用,防止内存泄漏 first = null; if (leader != null) { available.await(); // 已经有 Leader 在等,我只当 Follower } else { Thread thisThread = Thread.currentThread(); leader = thisThread; try { available.awaitNanos(delay); // 我是 Leader,精准休眠 } finally { if (leader == thisThread) leader = null; } } } } // 在 offer 方法中唤醒时: if (q.peek() == e) { leader = null; available.signal(); // 只唤醒一个 Follower 或唤醒 Leader 重新计算时间 } -
深度解析 :
DelayQueue通过Thread leader变量记录了唯一负责计时等待的线程。其他线程无限期挂起,避免了惊群。这是 单锁 + 条件队列 下的高级优化手段。
4.3 扩容机制:PriorityBlockingQueue 的 tryGrow 与锁分离
-
普通数组队列扩容问题 :如果扩容时持有主锁(
lock),那么在拷贝大数组(例如从 1GB 扩到 2GB)期间,所有读写操作全部阻塞,系统进入 Stop-The-World 状态。 -
PriorityBlockingQueue 的优化 :
javaprivate 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)); newArray = new Object[newCap]; } finally { allocationSpinLock = 0; } } // 如果没抢到扩容锁,说明其他线程在扩容,让出 CPU if (newArray == null) Thread.yield(); lock.lock(); // 重新获取主锁 // 将旧数据拷贝到新数组... } -
深度分析 :这里使用了
allocationSpinLock配合UNSAFE.compareAndSwapInt实现了一个轻量级自旋锁 。最关键的一步是lock.unlock(),它允许在内存分配(通常较慢,涉及 GC)期间,其他线程可以正常进行take和offer(如果旧数组还有空间)。这是一种 "扩容期间读写不阻塞" 的精巧设计。
4.4 排序与优先级:DelayQueue 时间轮与堆的结合思考
DelayQueue 底层是堆,时间复杂度 O(log n)。在大量延时任务(如几十万定时器)场景下,ScheduledThreadPoolExecutor 为何不直接使用 DelayQueue?
- ScheduledThreadPoolExecutor 实现 :它使用基于数组的时间轮(Hashed Wheel Timer) 的变种
DelayedWorkQueue(内部也是堆,但针对定时任务做了优化)。 - 对比 :
DelayQueue是通用的,每个元素都需要实现Delayed接口。而在高精度、海量定时任务中,分层时间轮 的入队出队复杂度接近 O(1),远优于堆的 O(log n)。这解释了为何 Netty 的HashedWheelTimer和 Kafka 的定时器都使用时间轮而非DelayQueue。但DelayQueue对于通用、少量、精确的延迟任务依然是最简单可靠的选择。
4.5 直接传递:LinkedTransferQueue 的松弛(Slack)特性解析
- SynchronousQueue 的缺陷 :严格手递手。如果生产者到达时没有消费者,生产者必须挂起。这在负载波动时会导致大量线程被挂起和唤醒,系统调用开销剧增。
- LinkedTransferQueue 的松弛(Slack) :
- 异步缓冲 :支持
put/offer,元素入队后生产者即可返回,不用等待消费者。此时数据节点挂载在链表上。 - 匹配消除 :当消费者调用
take时,它会遍历链表。如果遇到数据节点 ,直接取走数据并移除节点;如果遇到请求节点(即等待中的消费者),则帮助它完成匹配(数据直接从生产者传递给消费者)。 - 松弛特性(Slack) :允许数据在队列中短暂停留(缓冲),也允许请求在队列中等待。这种"不强制立即匹配"的策略极大地平滑了生产者和消费者的速率波动 ,减少了线程挂起次数,因此在高并发下吞吐量显著优于
SynchronousQueue和LinkedBlockingQueue。
- 异步缓冲 :支持
4.6 双端操作:工作窃取(Work Stealing)与 LinkedBlockingDeque
- 应用场景 :ForkJoinPool 中的每个工作线程都有自己的双端队列。
- 操作模式 :
- 本地线程(LIFO) :工作线程从头部 (
takeFirst)取任务。因为头部是最近加入的,缓存热,且 LIFO 利于深度优先递归。 - 窃取线程(FIFO) :当某线程空闲时,它随机挑选一个繁忙线程,从尾部 (
takeLast)窃取任务。这是最早加入的任务(粒度通常较大),有助于负载均衡。
- 本地线程(LIFO) :工作线程从头部 (
- 为何不用 LinkedBlockingDeque 直接作为 ForkJoinPool 的队列?
LinkedBlockingDeque是阻塞 的,且基于单锁。在窃取频繁时,单锁竞争非常激烈。- ForkJoinPool 内部使用的是
WorkQueue,这是一个基于数组、利用sun.misc.Contended消除伪共享的无锁双端队列 ,仅在必要时使用LockSupport.park阻塞。这再次印证了 针对特定场景专用优化 的设计原则。
4.7 公平性实现:SynchronousQueue 公平与非公平的性能取舍源码分析
-
非公平栈(TransferStack):
java// 匹配逻辑简示 SNode h = head; if (h != null && h.isFulfilling()) { // 帮助匹配 // ... } else if (h != null && h.mode != mode) { // 模式互补,尝试匹配栈顶 if (casHead(h, h.next)) { // 唤醒匹配的线程 } }为什么快? 栈顶数据是最后存入的,其对应的线程极大概率还在 CPU 核心上运行或处于缓存中(Cache Hot)。唤醒它几乎不需要从主存加载上下文,延迟极低。
-
公平队列(TransferQueue): 严格 FIFO。必须等待最前面的线程被匹配。这意味着被唤醒的线程可能早已被换出 CPU 缓存甚至被 Swap 到磁盘(极端情况),唤醒开销显著增加。
5. 性能特征总结(深度扩展:微观基准剖析)
5.1 内存屏障与伪共享的影响
- ArrayBlockingQueue 伪共享修复 :JDK 8 中,
ArrayBlockingQueue并未使用@Contended注解(这一点存疑,实际查看 JDK 8 源码确实未加)。但在高并发下,putIndex和takeIndex频繁修改确实会互相失效。实际测试中,单锁下这种伪共享被锁的互斥性掩盖了(因为不会同时修改)。但在 无锁数组队列(如 Disruptor)中,消除伪共享是性能翻倍的关键。 - LinkedBlockingQueue 的节点内存开销 :每个
Node对象头 12 字节(64位压缩指针),item引用 4 字节,next引用 4 字节,对齐填充 4 字节,共 24 字节 。积压 1000 万对象需额外占用 240MB 内存,且全部是堆内存垃圾,GC 压力巨大。
5.2 理论 JMH 吞吐量对比趋势(基于常见 Benchmark 结论)
| 队列 (1P-1C) | 吞吐量 (ops/sec) | 队列 (4P-4C) | 吞吐量 (ops/sec) |
|---|---|---|---|
| SynchronousQueue (非公平) | ~50M | LinkedTransferQueue | ~80M |
| LinkedBlockingQueue | ~45M | LinkedBlockingQueue | ~60M |
| ArrayBlockingQueue (非公平) | ~40M | ArrayBlockingQueue | ~20M (锁瓶颈) |
| PriorityBlockingQueue | ~5M (堆化开销) | DelayQueue | ~5M |
注:数据仅为展示相对数量级趋势,具体数值取决于硬件、JVM 参数、对象大小。
5.3 适用负载深度指南
- 极端低延迟交易系统 :
SynchronousQueue(配合忙等自旋,避免park系统调用)。 - ETL 数据管道(生产者极快,消费者慢) :必须使用有界
ArrayBlockingQueue。无界队列会导致内存撑爆,LinkedBlockingQueue会因 GC 停顿导致整体吞吐量波动剧烈。 - NIO 网络服务器(Netty) :
LinkedTransferQueue(或MpscQueue),因为需要批量提交任务且不希望阻塞 EventLoop 线程。
6. 选型指南(决策树扩展版)
除了基础的 Mermaid 决策树,这里补充反模式选型表:
| 如果你这样写... | 可能存在的问题 | 建议替换为... |
|---|---|---|
new LinkedBlockingQueue() |
无界队列,流量冲击时 OOM。 | new LinkedBlockingQueue(10000) |
在 DelayQueue 里存 10 万个定时任务 |
take 唤醒时惊群,性能差。 |
ScheduledThreadPoolExecutor 或 Netty HashedWheelTimer |
使用 SynchronousQueue 做缓冲 |
没有消费者时生产者直接阻塞,无法异步解耦。 | LinkedTransferQueue 或 ArrayBlockingQueue |
使用 PriorityBlockingQueue 且频繁遍历 |
iterator() 不保证顺序,业务逻辑错误。 |
先 drainTo 到 List 排序后再处理 |
| 在双端队列中使用双锁自定义实现 | 死锁和链表断裂风险极高。 | 直接用 LinkedBlockingDeque 或 ConcurrentLinkedDeque |
7. 实际应用场景综合举例(代码深度示例)
7.1 使用 LinkedTransferQueue 实现生产者-消费者零拷贝传输
java
// 场景:消费者需要等待生产者产出数据,且希望直接将数据所有权转移(Transfer)
public class ZeroCopyPipeline {
private final TransferQueue<DataBuffer> queue = new LinkedTransferQueue<>();
// 生产者线程
public void produce() {
DataBuffer buffer = new DataBuffer();
buffer.fillData();
// 阻塞直到有消费者取走这个 buffer,期间不会产生额外的内存拷贝
queue.transfer(buffer);
// 此时 buffer 已被消费者修改或释放,生产者不再持有
}
// 消费者线程
public void consume() {
while (true) {
DataBuffer buffer = queue.take(); // 如果无生产者,阻塞
buffer.process();
}
}
}
7.2 使用 DelayQueue 实现带重试的异步任务
java
public class RetryScheduler {
private final DelayQueue<RetryTask> queue = new DelayQueue<>();
class RetryTask implements Delayed {
private Runnable task;
private int retryCount;
private long nextRunTime; // 纳秒
// ... 实现 compareTo 和 getDelay
}
public void scheduleWithRetry(Runnable r, long delayMs, int maxRetries) {
queue.put(new RetryTask(r, maxRetries, System.nanoTime() + delayMs * 1_000_000));
}
public void startWorker() {
new Thread(() -> {
while (true) {
try {
RetryTask task = queue.take();
task.task.run();
if (task.retryCount-- > 0) {
task.nextRunTime += TimeUnit.SECONDS.toNanos(5); // 5秒后重试
queue.put(task); // 重新入队
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
}
8. 常见陷阱与最佳实践(深度剖析)
8.1 LinkedBlockingQueue 的 size() 是精确的吗?
源码证据:
java
private final AtomicInteger count = new AtomicInteger();
public int size() {
return count.get();
}
count 的修改受 putLock 和 takeLock 保护,由于是 AtomicInteger,读操作不加锁。结论 :size() 返回的值精确反映 了当时队列中的元素数量,不会像 ConcurrentLinkedQueue.size() 那样是 O(n) 弱一致。但在高并发下,你拿到 size=10 的瞬间,可能已经变成了 9 或 11,这是瞬时一致性问题,而非数据竞争错误。
8.2 SynchronousQueue 的 offer 与 poll 配对陷阱
java
SynchronousQueue<Integer> sq = new SynchronousQueue<>();
// 错误用法:试图先放再取
boolean offered = sq.offer(1); // 返回 false,因为没有等待的消费者
Integer val = sq.poll(); // 返回 null,因为没有等待的生产者
// 正确用法:生产者和消费者在不同线程并发调用
// Thread1: sq.put(1); // 阻塞
// Thread2: sq.take(); // 匹配成功,两者返回
8.3 PriorityBlockingQueue 比较器一致性问题
如果 Comparator 在元素入队后其排序依据字段发生变化(例如对象内部 priority 值被修改),二叉堆的结构不会被自动调整。必须 先 remove 再 add 来重新堆化,否则 take 的顺序是不可预测的。
9. 附录:源码关键片段索引(精确到 JDK 8 方法行号范围)
| 类名 (JDK 8) | 方法名 | 行号参考 (OpenJDK 8u) | 核心逻辑简述 |
|---|---|---|---|
| ArrayBlockingQueue | enqueue |
行 379-386 | 直接赋值 items[putIndex] = x,putIndex 自增及绕回。 |
| LinkedBlockingQueue | signalNotEmpty |
行 355-362 | 获取 takeLock 并 notEmpty.signal(),解决级联通知。 |
| PriorityBlockingQueue | tryGrow |
行 289-325 | 释放主锁,CAS 抢占 allocationSpinLock,分配新数组。 |
| DelayQueue | take |
行 235-283 | Leader-Follower 逻辑核心区域。 |
| SynchronousQueue | TransferStack.transfer |
行 230-340 | 处理 DATA 和 REQUEST 模式,包含自旋等待和超时处理。 |
| LinkedTransferQueue | xfer |
行 1300-1450 | 根据 how 参数(NOW, ASYNC, SYNC, TIMED)分流处理。 |
| LinkedBlockingDeque | linkFirst |
行 263-272 | 修改 first 指针及原头节点的 prev 指针。 |