atomic.LoadInt64能读到最新值是因为其隐含Acquire内存屏障语义,确保后续内存访问不被重排到该读之前,从而与atomic.StoreInt64的Release语义配对形成happens-before关系,保障跨goroutine的内存可见性与顺序一致性。

Go 的 atomic 操作默认就带内存屏障语义,你不需要手动写 mfence 或调用底层汇编——但必须清楚每种函数隐含的是哪种语义,否则照样出竞态。
atomic.Load* 为什么能读到最新值?它背后是 Acquire 语义
当你调用 atomic.LoadInt32(&x),Go 不只是原子读,还会插入 Acquire 内存屏障:它保证该读操作之后的所有内存访问(比如读另一个变量 y)不会被重排到这个 Load 之前。这解决了“看到 flag 为 true 却读不到对应数据”的经典问题。
- 典型场景:用
int32做状态标志,配合普通变量承载业务数据 - 错误写法:
if flag == 1 { return data }——flag和data都是非原子变量,编译器/CPU 可能重排,data还没写完就看到flag变了 - 正确写法:
if atomic.LoadInt32(&flag) == 1 { return atomic.LoadInt64(&data) }—— 第一个Load带 Acquire,确保后续读data看到的是它被写入后的值 - 注意:
atomic.Value.Load()是弱顺序的,不带 Acquire;若需同步非原子字段,得换atomic.LoadPointer或显式加runtime.GC()级别屏障(极少需要)
atomic.Store* 写完后,其他 goroutine 真的立刻能看到吗?Release 语义管什么
atomic.StoreInt64(&x, 42) 自动带 Release 语义:它保证该写操作之前的所有内存写入(比如先赋值 data = "hello",再 StoreInt32(&flag, 1))对其他 goroutine 可见,且不会被重排到 Store 后面。
- 关键点:Release 不保证“立即刷到主内存”,而是保证“在它之前的写,在它之后的读(Acquire)能看到”
- 常见误用:只在写端用
Store,读端却用普通读 ——flag可能被看到,但data仍为旧值,因为缺少 Acquire 约束读端顺序 - 配对原则:Release 写 + Acquire 读 才构成完整的 happens-before 链;单边使用无法建立可见性
- x86 上
Store实际可能不生成 mfence(用XCHG指令自带串行化),但语义等价;ARM 则必须插dmb ishst,Go runtime 自动处理
CompareAndSwap 和 Add 默认是 AcqRel,但别以为它能保护多字段
atomic.CompareAndSwapInt32(&x, old, new) 和 atomic.AddInt64(&counter, 1) 默认是 Acquire + Release(AcqRel):既约束前面的读写,也约束后面的读写。但这只针对单个变量 x 或 counter 本身。
立即学习“go语言免费学习笔记(深入)”;
- 典型坑:
atomic.StoreInt32(&a, 1); atomic.StoreInt32(&b, 2)—— 两个原子操作之间无顺序约束,其他 goroutine 可能看到a=1,b=0或a=0,b=2 - CAS 循环里改多个变量也不行:
old = atomic.LoadUint32(&x); if atomic.CompareAndSwapUint32(&x, old, old+1) { y++ }——y++非原子,且不在 CAS 保护范围内 - 真正需要多字段强一致时,要么用
sync.Mutex,要么把多个字段打包进 struct +atomic.Value(注意:struct 必须是可比较的,且Value.Store是整体替换) -
atomic.Add*性能虽高,但在高冲突下会自旋重试,CPU 占用可能飙升;而Mutex在争抢激烈时会让 goroutine 休眠,更省资源
最容易被忽略的点是:原子操作的内存序语义只作用于它直接读写的那个变量,以及通过 happens-before 链传递可见性的其他内存位置;它不自动“广播”整个 cache line 或 struct 的一致性。想靠 atomic 替代锁,必须从数据结构设计层面保证单变量语义足够表达业务原子性。


















