ArrayBlockingQueue高并发写入易卡住,因其所有操作共用同一把ReentrantLock,生产者密集put且队列满时持续争锁,拖慢消费者;适合固定容量、低线程数场景。

ArrayBlockingQueue 为什么在高并发写入时容易卡住
它底层用数组 + 可重入锁(ReentrantLock)实现,所有操作(put、take、offer、poll)都抢同一把锁。当生产者线程密集写入,又没空间时,put会阻塞并持续竞争锁,导致其他线程(包括消费者)被拖慢。
常见错误现象:Thread.getState() == BLOCKED 的线程数飙升,JVM 线程 dump 里一堆线程停在 ArrayBlockingQueue.lock.lockInterruptibly()。
- 适合场景:队列长度固定、生产消费速率接近、线程数少(≤4)、对内存连续性有要求(如实时系统缓存)
- 注意
capacity不能动态扩容,初始化后就锁死;设太小会频繁阻塞,太大浪费堆内存 - 公平模式(
new ArrayBlockingQueue(n, true))反而降低吞吐,除非你明确需要 FIFO 抢占顺序
LinkedBlockingQueue 的双锁机制到底省了什么
它用两个独立的 ReentrantLock:一个管 put(putLock),一个管 take(takeLock)。只要不同时满/空,生产和消费就能完全并发执行——这是性能分水岭。
使用场景:Web 请求队列、异步日志缓冲、消费者明显快于生产者的 pipeline。
立即学习“Java免费学习笔记(深入)”;
- 默认构造函数
new LinkedBlockingQueue()创建的是无界队列(Integer.MAX_VALUE容量),OOM 风险极高,务必显式传参,比如new LinkedBlockingQueue(1024) - 节点是链表,每次
put都要 newNode对象,GC 压力比ArrayBlockingQueue大,尤其在短生命周期高吞吐场景 -
size()是遍历链表计数,O(n),别在循环里调用;isEmpty()才是 O(1)
吞吐量差异在什么量级上才真正明显
单核 CPU 或低并发(70% 时,LinkedBlockingQueue 的吞吐通常高出 2–5 倍,延迟毛刺也更少。
关键不是“哪个更快”,而是“谁的瓶颈更符合你的压测曲线”:
- 如果你的压测中,
ArrayBlockingQueue的put平均耗时随线程数上升陡增,而LinkedBlockingQueue基本平稳 → 后者更适合 - 如果 GC 日志里
ParNew次数暴增,且对象主要来自Node→ 要么切回数组实现,要么加大 Young GC 空间 - JDK 8u292+ 开始,
ArrayBlockingQueue在某些 hotspot 优化下局部胜出(如极短队列 + 极高 cache 局部性),但属于边缘 case,别凭直觉猜
别忽略 BlockingQueue 的“阻塞”本身才是性能杀手
真正拖垮系统的,往往不是队列类型,而是业务逻辑让线程在 put 或 take 上等太久。比如下游服务 RT 从 10ms 涨到 500ms,ArrayBlockingQueue 会立刻堆积,LinkedBlockingQueue 只是撑得久一点。
- 永远配超时:用
offer(e, timeout, unit)替代put(e),用poll(timeout, unit)替代take() - 监控
remainingCapacity()和size()的比值,持续 >90% 就该告警或降级 - 如果发现消费者长期空闲、生产者却总在阻塞,说明瓶颈根本不在队列,而在消费者处理能力或下游依赖
队列只是缓冲区,不是性能银弹。选型只是开始,压测时盯住线程状态和 GC,比纠结类名重要得多。











