必须用atomicreference而非普通引用,因push/pop需原子性地读-算-写栈顶指针,cas保证原子性与可见性,配合final字段避免发布问题,并需防aba、空栈死循环及cpu自旋耗尽。

CAS 配合 AtomicReference 构建高并发无锁单向链表(如 Treiber Stack),核心在于用原子引用替代普通引用,把“读—算—写”三步压缩成一个不可分割的硬件级操作。它不靠锁排队,而是靠乐观重试:每次操作都假设没人干扰,失败就立刻再试——简单、轻量、适合高竞争短临界区场景。
为什么必须用 AtomicReference 而不是普通 Node 引用
栈顶指针(top)的更新本质是“先读当前节点 → 构造新节点 → 把新节点设为新顶点”三步。普通引用赋值(如 top = newNode)不是原子操作,多线程下极易丢失中间更新。而 AtomicReference 提供 compareAndSet(oldValue, newValue),底层调用 CPU 的 cmpxchg 指令,在缓存行级别完成比较并交换,天然具备原子性与可见性。
- 普通引用无法保证多个线程对
top的并发写入不互相覆盖 -
AtomicReference的 CAS 失败时返回false,必须显式检查并重试,不能忽略返回值 - 加
synchronized就违背了“无锁”设计初衷,也失去高吞吐优势
push 操作:构造安全 + 原子发布
入栈不仅要更新指针,还要确保新节点的 next 字段对其他线程立即可见。JVM 不保证构造器中字段写入的跨线程可见性,若在构造后单独赋值(如 node.next = oldTop),其他线程可能看到 next == null 的半初始化状态。
- 正确做法:将
next字段声明为final,并在构造器内一次性完成赋值 - 示例:
Node(E item, Node<e> next) { this.item = item; this.next = next; }</e> - 这样利用 Java 内存模型中的 final field semantics,确保新节点发布即安全
pop 操作:空栈防护 + 自旋节制
出栈看似只需一次 CAS,但实际要应对三种常见陷阱:空栈无限自旋、ABA 效应(虽不影响逻辑正确性,但可能触发 JVM unsafe 警告)、CPU 空转耗尽资源。
- 必须先调用
top.get()判断是否为null,再进入 CAS 循环;否则空栈时会陷入死循环 - 推荐使用带条件的循环结构,例如:
do { ... } while (!top.compareAndSet(current, next)) - Java 9+ 可在循环体内插入
Thread.onSpinWait(),提示 CPU 当前处于自旋等待,降低功耗与抢占开销
链表结构本身无需额外同步,但需避免逻辑耦合
Treiber Stack 的无锁性完全由栈顶指针的原子更新保障,链表中间节点不需要加锁或 volatile 修饰——因为所有修改都通过 top 的 CAS 完成,其他线程只能从顶部访问,不会直接操作中间节点。
- 不要在
Node中暴露可变的非 final 字段(如 public 可变next) - 不依赖链表长度、遍历等非原子操作做业务判断(如“栈大小 > 5 才允许 push”),这类需求需引入额外协调机制
- 若需防 ABA 影响(极少数涉及内存复用或弱一致性场景),可用
AtomicStampedReference替代,但 Treiber Stack 标准实现中并不必需
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











