sync.map读操作快是因为绝大多数读不加锁、不访问dirty表,直接查无锁的readonly;readonly是dirty的快照引用,未修改的entry指针被复用以节省内存分配。

Sync.Map 的读操作为什么快?只读表 readOnly 是怎么复用的
因为绝大多数读操作根本不会碰锁,也不访问 dirty 表。Sync.Map 在初始化后,只要没发生写操作,所有读都走 readOnly 字段——它是个无锁的 map[interface{}]interface{},且不带指针间接层(底层是直接哈希寻址)。
但要注意:readOnly 不是独立副本,而是对 dirty 的“快照引用”。当 dirty 被提升(upgrade)为新 readOnly 时,旧 readOnly 中的 entry 若未被修改过,会直接复用其指针;否则新建 entry。这种复用省了内存分配,但也意味着:如果某个 key 对应的 value 是大结构体指针,而你反复读它,它不会被 GC —— 因为 readOnly 持有引用。
-
Load先查readOnly.m,命中就返回;未命中再加锁查dirty - 若
readOnly.amended == false(即当前readOnly完全等价于dirty),未命中直接返回空,不查dirty - 若
readOnly.amended == true,说明dirty里有readOnly没覆盖的 key,此时必须加锁后查dirty
为什么第一次写会触发 dirty 初始化?misses 计数器怎么影响性能
Sync.Map 刻意延迟初始化 dirty,是为了避免低读高写的场景下白占内存。只有当第一次 Store 或 LoadOrStore 发生,且 dirty == nil 时,才把当前 readOnly.m 浅拷贝过去,并将所有 entry 的 p 字段置为非 nil(表示可读)。
真正影响性能的是 misses:每次读操作在 readOnly 未命中但 dirty 命中,misses 就加一;一旦 misses >= len(dirty),就触发 dirty → readOnly 提升(即把 dirty 整个赋给 readOnly,重置 misses = 0)。这个逻辑本意是“读多写少时尽快切回无锁路径”,但副作用明显:
立即学习“go语言免费学习笔记(深入)”;
- 如果写操作很密集(比如每秒上百次
Store),misses很难攒够,dirty长期膨胀,内存占用高 - 如果 key 分布极不均匀(比如 99% 请求都查同一个 miss key),
misses会暴涨,导致频繁 upgrade,反而增加锁竞争和 GC 压力 -
misses是无符号整型,溢出后归零,可能掩盖真实问题
Store 和 LoadOrStore 对 readOnly 的修改为什么不直接生效
它们从不直接改 readOnly.m,因为那是只读快照。所有写操作都只动 dirty,哪怕 key 已存在于 readOnly 中。
关键点在于 entry 的状态机:entry.p 可以是 nil、expunged(已清理标记)、或指向实际 value 的指针。当 Store(k, v) 执行时:
- 若
k在readOnly中存在且p != expunged,直接更新p指向新 value(无锁) - 若
p == expunged,说明该 key 已从dirty被删过,此时必须加锁,确保dirty中也补上这个 key - 若
k不在readOnly中,则一定去dirty写(加锁)
所以你看不到“更新 readOnly”的代码,是因为它靠 entry.p 的指针更新来间接生效,而 expunged 状态才是隔离 readOnly 和 dirty 生命周期的关键阀门。
什么时候该放弃 Sync.Map 改用 map + sync.RWMutex
不是“并发高就选 Sync.Map”,而是看读写比和 key 生命周期。Sync.Map 的优势只在“读远多于写 + key 集合长期稳定”的场景。一旦出现以下情况,原生 map 加 RWMutex 更简单、更可控:
- 写操作频率 > 100 次/秒,尤其伴随大量
Delete—— Sync.Map 的expunged处理和misses升级会吃掉可观 CPU - 需要遍历全部 key(
Range性能差,且结果不保证原子性) - value 是小结构体或需深拷贝,而你依赖 GC 及时回收 —— Sync.Map 的 entry 引用可能让对象驻留更久
- 要用
Map做嵌套(比如map[string]*sync.Map),维护成本陡增,不如统一用互斥锁
最常被忽略的一点:Sync.Map 的 zero value 是可用的,但它的内部字段(如 misses)是未导出的,没法做任何 introspect 或 reset。调试时发现 misses 卡在高位不动?只能重启或重构逻辑 —— 它的设计就是“用完即弃”,不是用来长期观测的。










