CAS在无锁栈中通过原子校验确保修改基于一致快照,避免覆盖写、链表断裂和nil panic;需结合版本号防ABA、内存序控制可见性,并用sync.Pool管理节点生命周期。

CAS 在无锁栈中不是“避免”指针碰撞,而是让碰撞可检测、可重试、不破坏结构。它不阻止多个线程同时操作,而是用原子校验确保每次修改都基于一致的快照,从而规避 覆盖写、链表断裂 和 nil panic 这三类典型异常。
靠 CAS 循环保证“读-改-写”的原子语义
直接赋值头指针(如 head = new_node)在并发下会丢失中间状态。CAS 要求每次更新前先读取当前值,构造新状态后尝试提交,失败就重读再试:
- Push 时:读当前
top→ 新节点next指向它 → 用 CAS 把top从旧值改为新节点 - Pop 时:读当前
top和top.next→ 用 CAS 把top从旧值改为top.next - 只要一次 CAS 成功,就说明该次修改没被其他线程干扰;失败则说明已被抢先更新,必须重来
用 unsafe.Pointer + uintptr 打包防 ABA
纯指针比较无法区分“同一个地址但不同生命周期”的节点,这就是 ABA 问题。Go 中没有带版本的原子指针,必须手动组合:
- 把指针转为
uintptr,高位存地址,低位(如低 8 位)存版本号 - 每次 CAS 前,用
atomic.LoadUintptr拆出当前指针和版本 - 构造新值时,地址部分不变,版本号加 1;CAS 要求地址和版本同时匹配才成功
- 节点必须堆分配(
&node{}),不能从栈或 slice 取地址,否则 Go 1.19+ 会拒绝转换
强制内存序与可见性控制
CAS 成功不代表其他 goroutine 立刻看到新结构。需要搭配恰当的内存序防止指令重排和缓存不一致:
- Push 成功后用
atomic.StorePointer写节点 next 字段时,需配合memory_order_release语义(Go 中通过atomic.StorePointer隐式保障) - Pop 读
top.next前,用atomic.LoadPointer加memory_order_acquire保证能看到完整初始化的节点 - 所有节点字段访问必须经由原子操作或指针解引用,不能绕过 —— 否则可能读到未写完的字段,引发 panic
GC 配合与节点生命周期管理
Go 的 GC 不会主动回收正在被 CAS 循环读取的节点,但也不能依赖它长期保活:
- Pop 出的节点不能立刻
free,要等确认没有 goroutine 正在 CAS 它的指针域 - 实践中常用对象池(
sync.Pool)复用节点,既避免频繁分配,又绕开 ABA 风险 - 不推荐用
runtime.SetFinalizer管理节点释放,因时机不可控,易导致悬垂指针


















