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

为什么 Treiber 栈必须用 AtomicReference 而不是普通引用
因为栈顶指针的更新必须是原子的:push 和 pop 都要「读当前顶点 → 计算新顶点 → 写回」三步,中间不能被其他线程插队。普通 Node 引用赋值不具备原子性,哪怕加 synchronized 也破坏了“无锁”前提。
AtomicReference 提供 compareAndSet(oldValue, newValue),底层调用 CPU 的 CAS 指令,失败就重试(自旋),不阻塞线程。
- 错误写法:
top = newNode—— 竞态下会丢失更新 - 正确模式:循环调用
compareAndSet(oldTop, newTop),直到成功 - 注意
compareAndSet返回boolean,必须检查返回值,不能忽略失败
push 操作中容易漏掉的内存可见性问题
CAS 本身保证可见性,但 Node 字段(如 next)的写入如果不配合 happens-before 关系,其他线程可能看到未初始化的字段值。JVM 不保证构造器内字段写入对其他线程立即可见。
- 必须把
next字段声明为final(推荐)或volatile - 错误示例:
Node node = new Node(value); node.next = oldTop; top.compareAndSet(oldTop, node)—— 中间两行无同步,node.next可能为 null - 正确做法:在构造器里完成
next赋值,且该字段为final,利用 final field semantics 保证发布安全
pop 自旋逻辑里最常写的死循环陷阱
看似简单的一次 CAS 尝试,实际要处理空栈、ABA 问题(虽不影响正确性,但影响性能)、以及无限自旋耗尽 CPU。
立即学习“Java免费学习笔记(深入)”;
- 必须先检查
top.get() == null,再进 CAS 循环,否则空栈时会永远自旋 - 不要在 while(true) 里裸跑
compareAndSet,至少加个Thread.onSpinWait()(Java 9+)缓解 CPU 占用 - ABA 问题在这里不破坏逻辑(Treiber 栈只关心引用相等),但若中间有节点被回收又复用,可能触发 JVM 的 unsafe 内存复用警告(极少见)
- 示例片段:
Node current, next;<br>do {<br> current = top.get();<br> if (current == null) return null;<br> next = current.next;<br>} while (!top.compareAndSet(current, next));<br>return current;
和 ConcurrentLinkedStack 对比时的关键差异点
Java 8+ 的 ConcurrentLinkedStack 本质就是 Treiber 栈的工业实现,但它做了大量优化:延迟初始化、批量回收、更精细的自旋退避。自己手写时容易低估这些细节。
- 它用
UNSAFE直接操作内存地址,绕过AtomicReference的一层封装,性能略高 - 它的
pop在多次失败后会短暂让出 CPU(不是yield,而是类似LockSupport.parkNanos(1)) - 自己实现时若没做退避,高争用下吞吐反而不如
Stack+synchronized - 除非你在写教学代码或嵌入式受限环境,否则直接用
ConcurrentLinkedStack更稳
真正难的不是写出能跑通的 CAS 循环,而是让自旋不卡死、内存不乱序、边界不崩溃——这些全藏在几行 if 和 while 后面。










