sync/atomic 更适合计数器场景,因其提供无锁、无调度开销的 cpu 级原子指令;但仅保证单操作原子性,不支持“先读再加”等复合操作,需慎用 cas 并避免类型混用与未对齐访问。

为什么 sync/atomic 比 sync.Mutex 更适合计数器场景
因为计数器本质是单变量读写,sync/atomic 提供 CPU 级原子指令(如 ADD、LOAD),无锁、无调度开销;而 sync.Mutex 会触发 goroutine 阻塞和唤醒,高并发下性能断崖式下降。
但要注意:原子操作只保证单个操作的原子性,不提供复合操作(比如“先读再加”)的原子性——这正是最容易误用的地方。
- 适用场景:
int32/int64/uint32/uint64/uintptr和指针类型,其他类型(如struct或float64)必须用unsafe.Pointer包装或改用sync.Mutex -
atomic.AddInt64要求变量地址对齐(int64需 8 字节对齐),在 struct 中若前面字段长度不是 8 倍数,可能 panic;建议单独声明或用//go:align 8注释(Go 1.21+) - 32 位系统上
atomic.AddInt64实际调用的是sync/atomic内部锁实现,性能不如 64 位系统原生指令,生产环境优先用 64 位 OS + GOARCH=amd64
atomic.LoadUint64 和 atomic.StoreUint64 为什么不能混用 int64
Go 的原子操作函数严格按类型区分,atomic.LoadUint64 只接受 *uint64,传入 *int64 会编译失败。看似只是符号差异,但底层指令和内存模型语义不同。
常见错误是把计数器定义为 int64,却用 atomic.LoadUint64(&counter) 强转指针——这属于未定义行为,可能读到错位字节或触发 panic。
立即学习“go语言免费学习笔记(深入)”;
- 统一用
uint64定义计数器变量(避免负值语义歧义,也规避符号扩展问题) - 如果必须用
int64(如与外部协议对接),就全程使用atomic.LoadInt64/atomic.AddInt64等对应函数组 - 切勿用
unsafe.Pointer强转类型绕过检查:Go 1.19+ 对原子操作的指针类型做了更严格校验,运行时可能 panic
高并发下 atomic.CompareAndSwapUint64 实现条件更新的坑
atomic.CompareAndSwapUint64 是唯一能做“读-改-写”原子复合操作的函数,常用于限流、状态机跃迁等场景。但它不是重试循环,失败后需手动处理逻辑分支。
典型错误是把它当“乐观锁 update”直接用,没包在 for 循环里,导致条件不满足时静默丢弃更新。
- 正确模式是“读取当前值 → 计算新值 → CAS 尝试 → 失败则重读重算”,例如实现最大值更新:
for {
old := atomic.LoadUint64(&maxVal)
if old >= newVal {
break
}
if atomic.CompareAndSwapUint64(&maxVal, old, newVal) {
break
}
}
unsafe.Pointer 实现链表等结构,仍需引入版本号字段为什么 atomic.Value 不适合存数字计数器
atomic.Value 专为“读多写少”的大对象安全发布设计(如配置、连接池),内部用互斥锁保护写操作,读操作虽无锁但有 iface 接口转换开销。它完全不适合高频增减的计数器。
有人图方便把 int64 包进 atomic.Value,结果发现吞吐量比裸 sync.Mutex 还低——因为每次 Store 都要分配新接口,每次 Load 都要类型断言。
- 数字类计数器必须用
atomic.LoadInt64/atomic.AddInt64等原生函数 -
atomic.Value只应在需要原子替换整个不可变结构体时使用,例如:atomic.Value存一个map[string]string配置快照 - Go 1.19+ 支持泛型
atomic.Value,但底层机制没变,别被语法糖误导
真正难的从来不是调哪个函数,而是想清楚“这个操作是否真的需要原子性”——比如日志计数器允许短暂不一致,用 sync/atomic 反而掩盖了数据精度需求;而支付余额更新漏一次 CAS,就是资损。边界模糊时,宁可锁得重一点,也别假定原子操作能兜住所有逻辑。










