ArrayBlockingQueue只用一把锁因其循环数组的put/take操作共享putIndex和takeIndex,且需协同通知notFull/notEmpty条件变量,拆锁会导致状态不一致;LinkedBlockingQueue用双锁分离生产消费路径,putLock管插入与count++,takeLock管移除与count--,count用volatile+锁保证安全,但频繁Node分配引发GC压力。

ArrayBlockingQueue 为什么只用一把锁
它底层是循环数组,所有操作都围绕同一块连续内存展开:put 和 take 都要更新 putIndex 和 takeIndex,还要判断是否满/空。这两个索引的读写不是独立的——比如 put 后要检查是否触发 notFull.signal(),take 后要检查是否触发 notEmpty.signal(),而这些条件变量又共用同一把 ReentrantLock。强行拆锁会导致状态不一致,比如生产者刚改完 putIndex、还没来得及通知消费者,就被另一个消费者抢到锁并读到了旧的 takeIndex,直接越界或漏数据。
LinkedBlockingQueue 的双锁是怎么分工的
它把生产和消费彻底隔开:putLock 只管链表尾部插入、维护 count 和 notFull;takeLock 只管头部移除、维护 count 和 notEmpty。两个锁各自持有自己的 Condition,互不干扰。关键点在于:count 字段虽然是共享的,但每次增减都加了 volatile 语义,且双锁在修改 count 前都会先获取对应锁,所以不会出现并发修改冲突。
-
put操作:只持putLock→ 更新last指针 + new Node +count+++ 判断是否唤醒notEmpty -
take操作:只持takeLock→ 更新first指针 +count--+ 判断是否唤醒notFull
锁颗粒度差异在堆栈里怎么体现
压测时看线程 dump 最直观:ArrayBlockingQueue.lock.lockInterruptibly() 附近会密集出现多个 Thread.State == BLOCKED 线程;而 LinkedBlockingQueue.putLock.lockInterruptibly() 和 LinkedBlockingQueue.takeLock.lockInterruptibly() 往往各占一拨,彼此不卡。
- 当队列既不满也不空时,
put和take完全无锁竞争,吞吐接近线性增长 - 一旦满,所有
put线程会在putLock上排队,但take不受影响;同理,空时只有take排队 - 注意:
size()是遍历链表计数,会同时 hold 住两个锁,千万别在高频路径里调用
别被“双锁”误导——真正影响性能的其实是内存分配模式
双锁只是让并发更顺,但 LinkedBlockingQueue 每次 put 都要 new 一个 Node 对象,短生命周期对象爆发式生成,会直接拉高 ParNew GC 频率;而 ArrayBlockingQueue 所有元素就地存放,零对象分配。如果你的业务每秒产生上万任务,GC 停顿可能比锁竞争还疼。
- 查 GC 日志时重点看
Allocation Rate和Young GC Count,不是只盯线程阻塞数 - 如果发现
ParNew次数飙升,且jmap -histo显示大量java.util.concurrent.LinkedBlockingQueue$Node,说明瓶颈其实在堆,不在锁 - JDK 8u292+ 虽然对 Node 分配做了缓存优化,但无法消除对象创建本身,这点不能靠版本升级绕过

















