concurrentlinkedqueue适合高吞吐、低延迟、允许弱一致性的场景,如日志缓冲、事件总线、异步任务暂存;它无阻塞、无锁、不保证强fifo,依赖cas和volatile实现,禁止null元素,易致内存泄漏。

ConcurrentLinkedQueue 适合什么场景
它不是万能的“并发安全队列”替代品。真正适合它的场景很窄:高吞吐、低延迟、允许弱一致性(比如日志缓冲、事件总线中间件、异步任务暂存),且调用方不关心阻塞等待。如果你需要 take() 等待元素、或 put() 不想丢数据,别硬上 ConcurrentLinkedQueue——它没有阻塞方法,也没容量限制,offer() 永远返回 true,哪怕 OOM 前一秒还在成功入队。
- 日志收集器中暂存待刷盘的
LogEvent对象 - Netty 的
ChannelOutboundBuffer底层用类似思路做无锁写队列 - 不要求严格 FIFO 顺序、能容忍短暂“看不见刚入队元素”的读写分离路径
为什么它不阻塞也不加锁
核心是基于 CAS + volatile + 指针跳转实现的无锁算法(Michael-Scott 算法变种)。节点入队时,只用一次 UNSAFE.compareAndSet() 更新 tail;出队时同理更新 head。没有 synchronized,也没有 AQS,所以不会挂起线程,也不会产生锁竞争开销。
- 所有操作都是非阻塞的:
poll()返回null表示空,不会等 - 内存可见性靠
volatile字段(item、next)保证,不是靠锁释放内存语义 - 会出现“幽灵节点”:已出队但还没被 next 指针跳过的节点,GC 前会暂时滞留
offer() 和 poll() 的实际行为差异
这两个方法看着像普通队列,但行为比 ArrayList 或 LinkedList 更“松散”。它们不保证强实时可见性,也不保证单次调用一定反映最新状态。
-
offer()总返回true,即使堆内存快耗尽——它不检查容量 -
poll()可能返回null,即使刚有线程offer()了元素(因 head 指针未及时推进) -
size()是个昂贵操作:要遍历链表计数,且结果可能瞬间过期;生产代码里应避免调用 -
isEmpty()相对便宜,但仍是两次 volatile 读,不是原子快照
容易踩的坑:null 元素和内存泄漏
ConcurrentLinkedQueue 明确禁止 null 元素——插入时抛 NullPointerException,不是静默忽略。这点和 ArrayBlockingQueue 不同,容易在迁移旧代码时翻车。
立即学习“Java免费学习笔记(深入)”;
- 队列节点对象本身不会自动清空引用,如果存的是大对象(比如 byte[]),出队后若外部还持有该节点,就可能阻碍 GC
- 使用 lambda 或匿名内部类往队列里塞 Runnable 时,注意隐式持有了外部类引用,容易放大泄漏面
- 它不支持批量操作:
addAll()是逐个offer(),失败时部分成功、部分失败,没法回滚
多线程环境下,它的“无锁”优势只在竞争激烈且操作轻量时明显;一旦节点构造成本高、或业务逻辑混在里面,CAS 失败重试的开销反而比一把公平锁更伤。别把它当银弹,先想清楚你要的到底是“不阻塞”,还是“不想要锁”。










