CAS在Michael-Scott无锁队列中通过AtomicReference对head/tail指针和节点next字段执行原子操作,实现无锁的入队(两次CAS追加+推进tail)与出队(CAS推进head并处理空队列),以自旋重试、滞后更新和volatile语义保障线程安全与内存可见性。

Java 中 CAS 在 Michael-Scott 无锁队列中的核心作用,是通过原子操作替代锁来保证多线程下入队(enqueue)和出队(dequeue)的线程安全。它不依赖 synchronized 或 ReentrantLock,而是靠 AtomicReference 对节点和指针进行 CAS 修改,关键在于正确处理 ABA 问题、空队列边界、以及 tail/head 的“滞后更新”策略。
入队(enqueue):双指针 + 两次 CAS 保障原子性
Michael-Scott 队列使用 head 和 tail 两个原子指针指向逻辑首尾节点。入队不是直接更新 tail,而是分两步:
- 先用 CAS 尝试将新节点插入到当前 tail 的 next 字段(即追加到链表末尾);
- 若成功,再用 CAS 尝试推进 tail 指针指向新节点;若失败(说明其他线程已更新 tail),则重试或跳过推进(允许 tail 滞后)。
这种“松弛 tail”设计避免了每次入队都必须更新 tail 带来的高竞争。典型实现中,tail 可能指向倒数第二个节点,只要保证从 tail 出发总能通过 next 找到真实尾节点即可(借助 helper 方法定位)。
出队(dequeue):head 推进 + 跳过已标记节点
出队同样依赖 CAS 推进 head 指针,但需处理两种情况:
- 正常情况:head 指向的节点有有效数据,直接 CAS 更新 head 到其 next,并返回原 head 的值;
- 并发干扰:其他线程刚完成入队,导致 head.next 为空(队列变空),或 head 已被其他 dequeue 线程“跳过”,此时需重新读取 head 并重试;
- 内存可见性:head 和 tail 的读取必须用
get()(等价于 volatile 读),CAS 操作天然具备 happens-before 关系。
注意:Michael-Scott 原始版本不标记已出队节点,因此不存在删除节点的 ABA 风险;但若扩展支持“逻辑删除”,需配合 AtomicStampedReference 或对象内嵌版本号规避 ABA。
CAS 失败后的重试逻辑:自旋而非阻塞
所有 CAS 操作失败时不抛异常、不挂起线程,而是进入轻量级自旋循环:
- 反复读取最新 head/tail 值,重新计算应写入的位置;
- 避免因短暂竞争导致线程调度开销,提升吞吐;
- 实际工程中可加入
Thread.onSpinWait()(Java 9+)提示 CPU 优化自旋行为。
自旋次数通常不限定,因为无锁算法假设竞争是瞬时的;极端高竞争下,可能退化为高 CPU 占用,但这属于场景约束而非实现缺陷。
Java 实现关键点:引用语义与内存模型对齐
Java 中需严格遵循 JSR-133 内存模型,确保 CAS 操作的原子性和可见性:
- 节点的 next 字段必须声明为
volatile,或通过AtomicReferenceFieldUpdater控制; - head/tail 使用
AtomicReference,所有读写走原子方法; - 构造新节点时,data 字段可在构造时赋值(不可变),next 初始化为 null,避免发布逸出;
- 禁止编译器或 CPU 重排序影响指针链接顺序(CAS 本身提供屏障,但初始化仍需注意)。
标准 JDK 并未提供 Michael-Scott 队列的公开实现,ConcurrentLinkedQueue 正是基于该算法的工业级实现,可参考其源码理解真实落地细节。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











