CAS是CPU级原子指令,通过比较内存值与预期值决定是否更新,避免锁开销;但存在ABA问题、循环开销大及仅支持单变量原子操作等缺陷,需结合版本号(如AtomicStampedReference)或有序写入等机制弥补。

为什么不能直接用 Unsafe.compareAndSwapObject 实现安全的无锁队列
直接调用 Unsafe.compareAndSwapObject 无法绕过 ABA 问题和内存可见性陷阱,更关键的是:它不提供顺序保证。JVM 可能重排序读写操作,导致生产者看到的 tail 指针已更新,但对应节点的 next 字段仍是 null —— 这会让消费者跳过刚入队的元素。
必须配合 Unsafe.putOrderedObject(或 Unsafe.putObjectVolatile)控制写入顺序,并用 Unsafe.getObjectVolatile 保证读取最新值。
-
compareAndSwapObject本身是原子的,但仅对单个字段有效;队列需协调head、tail、node.next多处状态 - 典型错误:在 CAS 更新
tail前,未用putOrderedObject先写死新节点的next - JDK 9+ 中
Unsafe的静态实例不再可通过getUnsafe()获取,需通过反射绕过检查(且仅限 boot classloader 加载的类)
如何构造线程安全的 Node 结构并避免伪共享
每个节点必须显式声明为 volatile 字段,否则 CAS 成功后其他线程可能读到旧的 next 值。同时要隔离 head 和 tail 引用,防止 CPU 缓存行争用。
示例节点定义(省略构造函数):
static final class Node {
volatile Node next;
final Object item;
Node(Object item) {
this.item = item;
}
}
-
next必须是volatile,不能依赖 CAS 后的隐式屏障 - 不要在 Node 内放
prev或额外状态字段,否则易触发伪共享;若需标记,用单独的 padded 字段(如@sun.misc.Contended) - 初始化时
next = null是安全的,但入队前必须先执行UNSAFE.putOrderedObject(node, NEXT_OFFSET, newNode)
入队逻辑中 CAS 与有序写入的精确顺序
标准 Michael-Scott 队列要求:先确保新节点的 next 已设为 null(或指向自身),再 CAS 更新 tail;但 CAS 失败时,必须重新读取当前 tail 并检查其 next 是否为空——否则会覆盖已有节点。
关键步骤(以单生产者为例):
- 分配新
Node,设置item - 调用
UNSAFE.putOrderedObject(newNode, NEXT_OFFSET, null)—— 确保后续读取next能见最新值 - 循环执行:
do { currentTail = UNSAFE.getObjectVolatile(tailRef); } while (!UNSAFE.compareAndSwapObject(this, tailRef, currentTail, newNode)) - 但仅此不够:需在 CAS 前确认
currentTail.next == null,否则说明已有其他线程抢先入队,此时应帮助推进 tail(即「helping」机制)
漏掉 getObjectVolatile 读取或误用普通读,会导致无限自旋或数据丢失。
出队时如何避免空指针与重复消费
出队需同时移动 head 并返回 head.next.item,但 head 本身是哨兵节点(dummy),真实数据在 head.next。常见错误是未检查 head.next == null 就解引用。
- 必须用
UNSAFE.getObjectVolatile(headRef)读 head,再用UNSAFE.getObjectVolatile(head, NEXT_OFFSET)读 next - 若
next == null,说明队列为空,直接返回 null(不可 CAS 更新 head) - 若
next != null,则尝试 CAShead指向next;失败说明其他线程已推进,继续循环 - 成功后,需再次读
next.item(因 item 是 final,无需 volatile,但必须在 CAS head 后读,防止读到旧节点的 item)
final 字段不解决发布问题 —— 如果节点对象未正确构造并发布,其他线程仍可能看到默认值。所以 putOrderedObject 写 next 是发布动作的关键一环。

















