sync.Map 与 atomic
本教程共 80 篇 · 第 64 篇 · 更新于 2026-07-27 · 约 8 分钟阅读
64. sync.Map 与 atomic
本节目标:学会用
sync.Map做并发安全的 map,掌握sync/atomic包的原子类型、CAS 操作和atomic.Value。
普通 map 不并发安全
内置 map 并发读写会直接 fatal:
m := make(map[int]int)
for i := 0; i < 10; i++ {
go func() {
m[1] = 1 // fatal error: concurrent map writes
}()
}
解决办法有两种:加锁,或用 sync.Map。
sync.Map
sync.Map 是开箱即用的并发安全 map,不用初始化:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
m.Store("a", 1) // 存
m.Store("b", 2)
v, ok := m.Load("a") // 取,返回值和是否存在
fmt.Println(v, ok) // 1 true
m.Delete("a") // 删
// 存在就返回旧值,不存在就存入
val, loaded := m.LoadOrStore("b", 3)
fmt.Println(val, loaded) // 2 true
// 删除并返回旧值
val, loaded = m.LoadAndDelete("b")
fmt.Println(val, loaded) // 2 true
// 遍历
m.Store("c", 3)
m.Range(func(k, v any) bool {
fmt.Println(k, v)
return true // 返回 false 可提前退出
})
}
什么时候用 sync.Map
sync.Map 比 map + Mutex 慢不少,但某些场景有优势:
- key 写入一次后反复读(配置缓存)
- 多个 goroutine 读写不同的 key(没有热点的 key)
如果是高频更新所有 key,map + RWMutex 通常更快。
Warning
sync.Map的 key 和 value 都是any类型,丢失了类型安全。取值后要类型断言:
v, _ := m.Load("count")
count := v.(int) // 断言成 int
atomic 包
sync/atomic 提供原子操作,用于简单数值的并发安全读写,比锁轻量。
原子操作是不可分割的,执行过程中不会被其他 goroutine 打断。适合计数器、标志位这类场景。
原子类型(Go 1.19+)
Go 1.19 起推荐用原子类型,比函数式 API 更直观:
var count atomic.Int64
func main() {
count.Store(0) // 存
count.Add(1) // 加
count.Add(5)
fmt.Println(count.Load()) // 6
old := count.Swap(10) // 交换,返回旧值
fmt.Println(old, count.Load()) // 6 10
}
支持的原子类型:
atomic.Boolatomic.Int32/atomic.Int64atomic.Uint32/atomic.Uint64atomic.Uintptratomic.Pointer[T](泛型,1.19+)atomic.Value(存任意类型)
每个类型都有 Load、Store、Swap 方法,整型还有 Add。
函数式 API
也可以用函数形式,操作普通变量:
var count int64
atomic.StoreInt64(&count, 0)
atomic.AddInt64(&count, 1)
fmt.Println(atomic.LoadInt64(&count))
新版推荐用原子类型,函数式 API 主要用于改老代码或操作结构体字段。
CAS 比较并交换
CAS(Compare-And-Swap)是无锁编程的核心。它比较内存值和期望值,相同才更新:
var count atomic.Int64
func add(n int64) {
for {
old := count.Load()
new := old + n
if count.CompareAndSwap(old, new) {
break // 成功
}
// 失败说明被别人改了,重试
}
}
CAS 的逻辑:读旧值 -> 算新值 -> 比较交换。如果期间没人改过,成功;否则重试。这就是乐观锁。
NoteCAS 适合「竞争不激烈、操作简单」的场景。竞争太激烈会频繁重试,反而比锁慢。另外 CAS 解决不了 ABA 问题(值从 A 变 B 又变 A),需要加版本号。
atomic.Value
atomic.Value 能存任意类型的值,用于「配置热更新」这类场景:
var config atomic.Value
func main() {
config.Store(&Config{timeout: 10})
go func() {
cfg := config.Load().(*Config) // 取出后类型断言
fmt.Println(cfg.timeout)
}()
// 热更新配置
config.Store(&Config{timeout: 30})
}
Warning
atomic.Value有两个限制:
- 不能存
nil- 前后存的值类型必须一致,否则 panic
原子 vs 互斥锁
| 特性 | atomic | Mutex |
|---|---|---|
| 适用场景 | 简单数值读写 | 复杂临界区 |
| 性能 | 高,无阻塞 | 有阻塞和调度开销 |
| 操作粒度 | 单个变量 | 一段代码 |
| 能否保护多变量 | 不能 | 能 |
Tip简单计数器用
atomic.Int64,保护多个关联变量用Mutex。别用原子操作去模拟复杂逻辑,容易出错。
原子类型不能复制
和锁一样,原子类型包含内部状态,复制后就不是同一个了:
// 错误
func bad(c atomic.Int64) {
c.Add(1) // 改的是副本
}
// 正确
func good(c *atomic.Int64) {
c.Add(1)
}
小结
sync.Map:并发安全 map,适合读多写少、key 不热点- key/value 是
any,取值要类型断言 atomic原子类型:Int64、Bool、Pointer[T]等,比锁轻量- CAS 比较交换实现乐观锁,竞争不激烈时高效
atomic.Value存任意类型,用于配置热更新,类型须一致- 原子类型和锁都不能复制,用指针传递
下一节进入 context 包,它是控制 goroutine 生命周期的核心工具。