arrayblockingqueue单锁非瓶颈,因锁持有时间极短、唤醒精准且无虚假唤醒;在生产消费频率对等时全程走fast-path,吞吐高于公平锁;capacity过小会引发频繁阻塞,应按突发量×1.5估算。

ArrayBlockingQueue 的单锁设计为什么不是性能瓶颈?
很多人看到 ReentrantLock 一把独占锁,第一反应是“读写互斥 → 瓶颈”,但实际在生产者与消费者频率对等、节奏稳定的场景下,这把锁反而贡献了最高执行效能。关键不在“有没有锁”,而在“锁的持有时间极短 + 唤醒精准 + 无虚假唤醒开销”。
它的锁只用于保护三个动作:更新索引(putIndex/takeIndex)、修改计数(count)、数组赋值/取值。全部是 O(1) 操作,没有遍历、没有扩容、没有对象创建。锁释放后立刻 notify 对应的 notEmpty 或 notFull,等待线程被唤醒后几乎立即抢到锁继续 —— 这种“短临界区 + 精准条件唤醒”的组合,比用两把锁(如读写锁)还少一次锁竞争判断。
生产者消费者频率对等时,put/take 成对触发的执行链路
当生产者每 put 一个元素,消费者紧跟着 take 一个,整个队列长期维持在「非空且非满」状态,此时 put() 和 take() 都不会进入 await 分支,全程走 fast-path:
-
put():加锁 → 检查count (真)→ <code>items[putIndex] = e→putIndex = (putIndex + 1) % items.length→count++→notEmpty.signal()→ 解锁 -
take():加锁 → 检查count > 0(真)→e = items[takeIndex]→takeIndex = (takeIndex + 1) % items.length→count--→notFull.signal()→ 解锁
两次调用加起来,锁持有时间通常在纳秒级。循环数组的局部性也利于 CPU 缓存命中,items、takeIndex、putIndex、count 四个字段在内存中紧凑排列,一次 cache line 基本能覆盖。
为什么公平锁(fair=true)在这种场景下反而拖慢吞吐?
公平锁强制线程按 FIFO 顺序获取锁,看似“更合理”,但在频率对等场景下会引入确定性延迟:
- 刚完成
take()的消费者释放锁后,即使生产者已在锁队列头,也要等下一个调度周期才能重入 —— 多一次上下文切换 - 而默认非公平锁允许“插队”:消费者刚释放锁,生产者可能直接抢到,立刻执行
put(),然后 signal 消费者;消费者被唤醒后又大概率能立即抢锁,形成近乎流水线的节奏 - 实测在 4 核机器上,同等负载下非公平模式吞吐常高出 15%~25%
容易被忽略的性能陷阱:capacity 设置不当
单锁高效的前提是「不频繁阻塞」。如果 capacity 设得太小(比如 2),哪怕频率对等,也会因瞬时抖动导致频繁触发 notFull.await() / notEmpty.await(),唤醒+调度开销迅速吞噬锁本身的低开销:
- 容量为 2 时,只要生产者快 1 个节拍,就满;消费者快 1 个节拍,就空 —— 实际变成“生产-阻塞-唤醒-消费-阻塞-唤醒”循环
- 推荐初始值按「平均单次 burst 量 × 1.5」估算,例如每秒稳定 1000 次 put/take,突发峰值约 1500,可设
capacity=2048 - 注意:capacity 是数组长度,不是字节数;过大虽不OOM,但会浪费 cache line,尤其当元素是大对象时
真正决定 ArrayBlockingQueue 在对等场景下能否跑出理论峰值的,从来不是锁本身,而是你有没有让它的循环数组始终“转得起来”—— 不卡住、不空转、不频繁换道。










