sync.Map 适合什么场景?它的底层原理是怎样的?
结论先行
sync.Map 是“为特定并发形态优化”的并发安全 map:适合读极多、写很少,或多个 goroutine 各自操作不相交 key 集合的场景。它做读写分离:无锁读为主,写与删除需要加锁,并可能触发一次从 dirty 到 read 的整表拷贝提升——通用性能未必优于“Mutex + map”,选型要看场景。
要点
- 官方口径:相比 RWMutex+map,sync.Map 只在两类场景更优:① key 基本只写一次、大量并发读(近似只读缓存、配置表);② 多个 goroutine 各自读写互不相交的 key。
- 适用典型:全局注册表、热点配置、连接池元数据、计数器聚合(LoadOrStore/Add)等。
- 不适用场景:写入频繁、需要 len()、需要一致快照遍历、value 需要多步读改写——这些用普通 map + 锁更简单高效。
- 原理骨架:内部 read 是一个 atomic.Value 保存的只读 map,元素为指针(entry),读走无锁快路径。
- 写路径:对已存在的 entry 尝试 CAS 更新,否则加锁把数据写进 dirty;读 miss 计数超过阈值时,把 dirty 整体拷贝提升为新的 read。
- 删除处理:entry 打上 expunged 标记延迟清理,删除不立即锁全表;遍历用 Range 回调,不保证与并发写一致的快照。
- API 面:Load/Store/LoadOrStore/Delete/Range/CompareAndSwap 等,没有 Len 方法。
示例
var registry sync.Map
// 近似只读注册表:写一次,到处并发读
registry.Store("module-a", "handler-a")
v, ok := registry.Load("module-a") // 无锁快路径
if !ok {
v, _ = registry.LoadOrStore("module-a", "handler-a")
}
_ = v
常见追问/记忆点
- 追问:sync.Map 一定比加锁 map 快吗?答:不一定;写多、需要 len 或一致遍历时通常更慢,先按场景选型。
- 追问:Range 能拿到一致快照吗?答:不能,它是尽力而为的遍历,可能看到并发写的中间状态。
- 追问:为什么没有 Len?答:并发下长度本身是瞬态概念,官方鼓励用 Range 统计或维护自己的计数。
- 记忆:读多写少或 key 不相交才上 sync.Map;否则 Mutex+map 更直白可控。