Lock-Free 的核心是任意线程崩溃或暂停都不阻塞其他线程,sync.Mutex 不满足此定义;真正 Lock-Free 必须仅依赖原子指令(如 CAS)和内存模型保证,Go 中需用 atomic 包配合 uintptr 打包指针与版本号防 ABA,且 GC 有隐式保障但禁止长期保存 uintptr。

为什么直接用 sync.Mutex 就不算 Lock-Free
Lock-Free 的核心不是“没加锁”,而是“任意线程崩溃或暂停,都不阻塞其他线程完成操作”。sync.Mutex 一旦持有者 panic 或被抢占,其他 goroutine 就会卡在 Lock() 上——这违反 Lock-Free 定义。真正 Lock-Free 必须只依赖原子指令(如 CAS)和无等待的内存模型保证。
Go 的 atomic 包提供 CompareAndSwapPointer、LoadPointer、StorePointer 等,是构建 Lock-Free 栈的唯一底层支撑。但要注意:atomic 操作的对象必须是 unsafe.Pointer,不能直接对结构体字段做原子读写。
节点设计必须带版本号防 ABA 问题
无锁栈最经典的坑是 ABA:线程 A 读到 top = X,被调度;线程 B 把 X 弹出、又压入一个新节点 Y(地址碰巧也等于 X);A 恢复后 CAS 成功,却误认为栈顶仍是原 X。Go 没有内置带版本的原子指针(如 C++ 的 std::atomic<shared_ptr>),必须手动把指针和计数打包。
- 用
uintptr组合地址 + 版本号:高位存指针,低位存版本(如低 8 位为版本,剩余为指针) - 每次 CAS 前必须先
atomic.LoadUintptr拆出当前指针和版本,再构造新值 - 不能用
unsafe.Pointer直接参与算术运算,必须转成uintptr再位运算
示例节点结构:
立即学习“go语言免费学习笔记(深入)”;
type node struct {
value interface{}
next *node
}
<p>type lockFreeStack struct {
head uintptr // uintptr{ptr: *node, version: uint8}
}
Push 和 Pop 的 CAS 循环怎么写才安全
CAS 是乐观策略,失败就重试。关键在于每次循环都重新读最新状态,而不是缓存旧值。尤其 Pop 必须处理空栈情况,且不能让 next 指向已释放内存(Go 有 GC,但节点仍可能被复用,需确保逻辑上不可见)。
-
Push:读当前head→ 构造新节点 → 设置new.next = oldPtr→ CAS 更新head;失败则重读 -
Pop:读当前head→ 若为空返回 nil → 拆出oldPtr和oldNext→ CAS 尝试将head改为oldNext;失败则重读 - 所有 CAS 都要检查返回值,
false表示已被其他线程抢先修改,必须跳回开头 - 不要在 CAS 循环里做耗时操作(如接口转换、分配内存),否则拖慢整个栈
典型 Pop 片段:
func (s *lockFreeStack) Pop() interface{} {
for {
head := atomic.LoadUintptr(&s.head)
if head == 0 {
return nil
}
ptr := unsafe.Pointer(uintptr(head) &^ (1<<8 - 1)) // 清掉低 8 位
n := (*node)(ptr)
next := uintptr(unsafe.Pointer(n.next)) | ((head + 1) & (1<<8 - 1))
if atomic.CompareAndSwapUintptr(&s.head, head, next) {
return n.value
}
}
}
GC 可能导致悬垂指针?Go 的 runtime 有隐式保障
有人担心:节点被 Pop 后,指针还留在某个 goroutine 的寄存器里,此时 GC 回收了该内存,后续解引用就 crash。但 Go 的 1.5+ runtime 在 STW 阶段会扫描所有 goroutine 的栈和寄存器,确保正在被原子操作引用的内存不会被回收——前提是节点指针始终通过 unsafe.Pointer 传递,且没有被转成裸 uintptr 后长期保存。
- 禁止把
unsafe.Pointer转成uintptr后存到全局变量或结构体字段中 - 所有
unsafe.Pointer→uintptr转换,仅限 CAS 操作内部临时使用 - 如果真需要长期持有地址,应改用
runtime.SetFinalizer延迟回收,但这会破坏 Lock-Free 语义
实际项目中,更常见的问题是内存泄漏:频繁 Push/Pop 导致大量短命对象触发 GC 压力。Lock-Free 栈本身不解决这个问题,得配合对象池(sync.Pool)复用节点。


















