sync.Map 的 read map 通过 atomic.Value 包裹 readOnly 结构实现无锁读,仅对存在且未被删的 key 成立;key 为 expunged 或不存在时会 fallback 加锁读 dirty。

sync.Map 的 read map 是怎么做到无锁读的?
它靠 atomic.Value 包裹一个只读结构体 readOnly,里面存的是普通 map[interface{}]*entry。只要不触发晋升(即 read 和 dirty 未同步),所有 Load 都直接原子读这个 map,完全不碰锁。
但注意:这个“无锁”只对「存在且未被删」的 key 成立。如果 key 在 read 里被标记为 expunged(删过但还没清理),或者压根不在 read 里,Load 就会 fallback 到加锁读 dirty —— 这时就不是无锁了。
常见错误现象:Load 延迟高、CPU 毛刺,往往是因为 misses 累积触发了 dirty → read 全量晋升,而晋升过程要锁住整个 dirty 并复制全部有效 entry。
Store 新 key 为什么比 mutex + map 慢?
Store 写新 key 时,必须走 dirty 路径,且要先检查 amended 标志:
- 若
amended == false(read已同步完dirty),就得先加m.mu锁,再把dirty整个赋给read,再清空dirty,最后才写入——这是一次 O(N) 操作; - 若
amended == true,可直接写dirty,但后续第一次Load该 key 仍需加锁去dirty查; - 相比之下,
sync.RWMutex+ 原生map的Store就是简单加写锁、插入、解锁,开销稳定。
性能影响:实测中,高频写新 key 场景下,sync.Map 的 Store 吞吐可能比 RWMutex + map 低 2–3 倍。
LoadOrStore 看似原子,其实有隐藏条件
LoadOrStore 不是“查不到就设”,而是“查不到且没被删过才设”。它的行为取决于 key 在 read 中的状态:
- key 存在且值非
nil→ 直接返回,不写; - key 存在但值是
expunged(之前调过Delete)→ 返回nil, false,**不写入**; - key 完全不存在 → 写入
dirty,下次Load才能见到。
容易踩的坑:用 LoadOrStore 实现“确保 key 存在”,结果发现删过的 key 死活加不回来。正确做法是明确需要初始化一次时才用它;否则,用 Load + Store 组合更可控。
Range 不是遍历,是快照 + 全局锁
Range 内部会先加锁,把当前 read 和 dirty 的所有有效 entry 拷贝到一个临时 slice,再解锁,最后逐个回调。这意味着:
- 你无法在回调里调
Delete或Store,改了也白改; - 看不到刚写入
dirty但还没晋升的 key; - 如果数据量大,拷贝本身就有开销,还可能阻塞其他写操作。
典型误用:拿 Range 做 session 过期清理——每次执行都锁全表,且漏删率高。真要清理,老实用 RWMutex + map 配合定时器。
真正关键的点不是“读多写少”这个结论,而是 sync.Map 把「读路径是否加锁」和「写路径是否触发晋升」这两个开关,绑在了同一个变量 misses 上。一旦写频次拉高,misses 就涨得快,锁就来得勤,性能拐点出现得比预想中早得多。


















