ConcurrentLinkedQueue 通过 CAS 循环重试实现无锁并发:入队分“连节点+推尾指针”两步,tail 为滞后缓存视图以降冲突,傀儡节点隔离 head/tail 操作域防 ABA,失败立即重试不阻塞。

Java 中无锁队列(如 ConcurrentLinkedQueue)不靠锁,而是靠 CAS 原子操作协调多个线程同时入队时的竞争。核心不是“避免冲突”,而是“允许冲突发生,再用 CAS 重试 + 结构设计来收敛结果”。
入队本质是两个 CAS 步骤:连节点 + 推尾指针
单次 offer() 并非一次写完,它拆成两步原子操作:
- 第一步:尝试把新节点设为当前
tail.next—— 这步 CAS 失败,说明已有别的线程抢先连上了 - 第二步:若第一步失败,先用 CAS 把
tail推到更靠后的节点(可能是刚被别人连上的那个),再重试第一步
这种“先连后推”或“先推再连”的循环重试机制,让多个线程能各自推进队列结构,而无需阻塞等待。
tail 指针不总指向真实队尾,这是有意为之的设计
tail 不是强实时的“最后元素指针”,而是一个“尽力靠近队尾”的缓存视图。它的滞后性降低了 CAS 冲突概率:
Java Maven 项目审查工作流的通用输入处理。当 Java Maven 项目以 ZIP压缩包或 GitLab 仓库 URL 形式提供且需要...时使用。
立即学习“Java免费学习笔记(深入)”;
- 高并发下,若每个线程都争着更新同一个
tail,失败率极高;延迟更新可分散 CAS 热点 - 哪怕
tail滞后一两个节点,只要最终能通过循环读取最新值并重试,逻辑正确性不受影响 - 这相当于用空间换时间:多走几步 CAS,换来整体吞吐提升
傀儡节点隔离 ABA 风险,让 head/tail 更新互不干扰
队列初始化时,head 和 tail 都指向一个空的傀儡节点(dummy node)。这个设计不只是为了统一边界处理:
- 它把“逻辑头节点”和“物理头节点”解耦,
poll()修改的是数据节点状态,而head的推进是异步的 - 当多个线程并发
poll(),不会因为直接 CAShead到不同数据节点而引发 ABA 误判(比如 head 从 A→B→A) - 入队只操作
tail及其next,出队只操作head及其next,两者 CAS 地址域完全分离,天然避免交叉干扰
失败即重试,不挂起、不阻塞、不依赖锁膨胀
CAS 失败后,线程不做任何等待,立即重新读取最新的 tail,再尝试连接。整个过程没有 synchronized、没有 park/unpark、不触发线程调度:
- CPU 资源消耗集中在自旋比较上,适合短临界区场景
- 在 NUMA 架构或多 socket 机器上,频繁读写同一 cache line 的
tail可能引发 false sharing,但这是性能调优问题,不是正确性问题 - 只要内存模型保证 volatile 语义(JMM 对
AtomicReference的要求),CAS 循环终将收敛

















