concurrentlinkedqueue的“无锁”指不使用传统锁,而依赖cas原子操作与volatile可见性实现线程安全;其核心是傀儡头尾节点与volatile链式结构,通过两次cas完成offer,重试机制保障head/tail推进,poll返回null但size>0属正常设计。

ConcurrentLinkedQueue 是 Java 并发包(JUC)中典型的无锁高性能队列,其核心在于 Michael-Scott(MS)非阻塞算法的工程化实现。它不依赖锁,全程通过 CAS 操作协调多线程对链表的修改,从而在高争用场景下保持低延迟与高吞吐。
head 和 tail 为何不直接指向真实首尾元素?
head 始终指向一个 哑节点(dummy node),即 item == null 的哨兵节点;tail 则可能滞后——有时指向最后一个真实节点,有时指向倒数第二个,甚至可能和 head 重合。这不是设计缺陷,而是 MS 算法的关键安全机制:
- 出队(poll)时,先用 CAS 尝试将 head 从哑节点推进到下一个节点,再提取该节点的 item;若失败,说明其他线程已推进过,只需重读 head 继续操作
- 入队(offer)时,通过定位“真正尾部”(q == null 的节点)插入新节点,再尝试更新 tail;tail 的惰性更新(relaxed update)减少了 CAS 冲突,也避免了为同步 tail 而引入额外协助逻辑
- 这种结构让 offer/poll 都能在无锁前提下完成“读–改–写”循环,且无需全局状态校验
如何天然规避 ABA 问题?
ABA 在无锁编程中常导致误判:某线程看到地址 A → 其他线程将 A 改为 B 再改回 A → 原线程 CAS 成功但语义错误。ConcurrentLinkedQueue 的解法简洁而有效:
- 所有 Node 实例均为一次性使用:一旦被 poll 出队,节点不再复用、不 reset、不回收进对象池
- CAS 比较的是对象引用(内存地址),而非 item 值;即使两个节点 item 相同,只要地址不同,CAS 就不会成功
- 无需引入版本号、epoch 或 Hazard Pointer 等复杂机制,既轻量又可靠
offer 为什么比 poll 更快?
二者虽都基于 CAS,但执行路径与竞争强度差异明显:
- offer 大多数情况下仅需一次 CAS:设置 tail.next,再尝试更新 tail(后者失败可忽略)
- poll 至少涉及两次 CAS:先推进 head(head = head.next),再清空原 head.next.item(防止内存泄漏和 GC 压力)
- 当队列只剩一个真实节点时(head == tail),poll 还需额外校验并重置 tail,进一步增加失败重试概率
- 实测表明,在多消费者高争用场景下,poll 的 CAS 失败率显著高于 offer,性能差距可拉大至 2–3 倍
适用与不适用的典型场景
它不是万能队列,优势明确,边界也清晰:
- 适合:单生产者多消费者、任务调度中枢、秒杀系统中的请求缓冲、日志异步刷盘等强调吞吐与低延迟的场景
- 不适合:需要容量控制(它无界)、需阻塞等待(如 take() 会挂起)、要求强一致性(迭代器只反映创建瞬间的快照)、或存在频繁复用节点需求(如自定义对象池)
- 替代建议:要阻塞选 LinkedBlockingQueue,要容量限制选 ArrayBlockingQueue,要双端操作选 ConcurrentLinkedDeque










