atomic.LoadInt64读不到最新值是因为普通读写绕过内存屏障,必须与atomic.StoreInt64配对使用;atomic.AddInt64是硬件级原子指令,替代非原子的counter++;CAS失败是正常并发反馈,需循环重试;指针原子操作须严格类型一致;原子变量必须有稳定地址。

atomic.LoadInt64 读不到最新值?一定是读写没配对
直接用 *p 读一个被 atomic.StoreInt64 写过的变量,大概率读到旧值——这不是 bug,是绕过了内存屏障。Go 的 atomic 操作靠底层指令加内存序保证可见性,普通读写不参与这套同步机制。
- 写必须用
atomic.StoreInt64(&x, v),读就必须用atomic.LoadInt64(&x),不能混用 - 哪怕
x是包级变量、类型完全匹配,fmt.Println(x)也属于“普通读”,不可靠 - ARM64 或低负载机器上更容易复现“卡在旧值”,因为缺少强制刷新缓存的语义
为什么 atomic.AddInt64(&counter, 1) 能替代 counter++?
counter++ 在汇编层是“读 → 加1 → 写”三步,中间可能被其他 goroutine 插入;而 atomic.AddInt64 是单条 CPU 原子指令(如 x86 的 LOCK XADD),硬件保证整个操作不可分割。
- 适用于高并发、低竞争场景:请求计数、指标打点、连接数统计
- 注意:
int64在 32 位系统上不是自然对齐的,必须用atomic函数访问,否则 panic 或数据错乱 - 别传
*int——int是平台相关类型,应显式用int64或int32,再取地址
CompareAndSwapInt64 不成功?不是失败,是并发控制的正常反馈
atomic.CompareAndSwapInt64(&x, old, new) 返回 false,只说明“此刻 x 的值不是 old”,不代表出错。这是无锁编程的设计前提,你得自己决定是否重试。
- 典型用法是 for 循环 + CAS 自旋,比如实现“只在小于阈值时递增”
- 高竞争下别空转:加
runtime.Gosched()让出时间片,或短休眠(time.Sleep(1ns))缓解 CPU 占用 - 别在循环里反复
var tmp int64 = atomic.LoadInt64(&x)然后传地址——局部变量地址生命周期不够,行为未定义
指针原子操作:StorePointer / LoadPointer 必须严格类型一致
atomic.StorePointer 和 atomic.LoadPointer 操作的是 unsafe.Pointer,不检查类型。存了 *Config 却当 *User 读,会直接崩溃或读出垃圾内存。
立即学习“go语言免费学习笔记(深入)”;
- 存的时候:用
unsafe.Pointer(&cfg),其中cfg是具体类型的变量或指针 - 读的时候:必须用
(*Config)(atomic.LoadPointer(&ptr)),括号里的类型要和存入时完全一致 - 跨函数传参时,别裸传
unsafe.Pointer,容易丢失类型上下文;Go 1.19+ 可用泛型封装一层类型安全 wrapper
最常被忽略的一点:所有原子变量必须有稳定地址。包级变量 OK,new(uint64) 分配的堆内存 OK,但切片元素、map value、结构体非导出字段、局部变量取地址——全都危险。不是编译不过,是运行时行为不可控。










