sync.Map 通过两个物理 map(read 和 dirty)、一个原子指针及互斥锁实现读写分离,专为“读多写少”场景优化;read 为只读哈希表,Load 优先无锁访问,未命中才锁查 dirty;Store 和 Delete 均以标记、延迟清理和动态晋升机制协同工作,写频繁时性能反不如普通 map + sync.RWMutex。

sync.Map 的读写分离不是靠抽象概念实现的,而是靠两个物理 map + 一个原子指针 + 一个互斥锁硬生生拆出来的。 它不追求通用性,只针对「读多写少」场景做极致优化。用错地方反而比普通 map + sync.RWMutex 更慢。
read 和 dirty 是两个独立的 map,不是视图或副本
很多人误以为 read 是 dirty 的快照,其实它们是完全隔离的存储结构:
-
read是atomic.Pointer[readOnly]指向的一个只读哈希表(map[any]*entry),所有读操作优先走它,全程无锁 -
dirty是一个普通map[any]*entry,所有写操作(Store、Delete)都必须先加mu锁,再操作它 - 二者之间没有自动同步机制,只有当
misses计数器溢出时,才会把整个dirty提升为新的read,并新建空dirty
Load 为什么快?因为大多数时候只查 read
Load 的执行路径是:先原子读 read.m[key] → 命中就返回;未命中才加锁查 dirty[key],同时 misses++。这意味着:
- 99% 的热 key 都在
read里,Load就是纯原子内存访问,没锁、没分支、没函数调用开销 - 冷 key 查
dirty会触发锁竞争,但这是可控代价——只要misses不频繁溢出,就不会反复晋升 - 注意:
Load对已标记删除(p == expunged)的entry会直接返回(nil, false),哪怕它还在dirty里
Store 触发晋升的条件很具体
Store 并不总是写 dirty。它会先尝试写 read.m[key](如果 amended == false 且 key 存在),失败才 fallback 到 dirty。而晋升(dirty → read)只在以下情况发生:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
misses达到当前read.m长度(不是固定值,是动态阈值) - 此时会原子替换
read指针,并把原dirty赋给新read.m,再新建空dirty - 晋升过程本身不阻塞读——旧
read仍可被并发读取,直到指针被原子更新 - 但如果写压力持续很高,
misses会不断触发晋升,导致大量内存拷贝和 GC 压力
delete 不是真的删,而是标记 + 延迟清理
Delete 的行为容易被误解:
- 它先尝试原子设置
read.m[key].p = nil;若失败(key 不在read或amended == true),就加锁操作dirty - 对
dirty中的 entry,它不是delete(dirty, key),而是设e.p = expunged - 真正从
dirty中移除该 entry,要等到下次晋升时——遍历dirty,跳过所有p == expunged的项 - 所以刚
Delete后立刻Load,仍可能拿到旧值(如果它还在dirty且未晋升)
真正关键的点在于:sync.Map 的优化全部建立在「读远多于写」这个假设上。一旦写比例超过 10%,或者 key 分布极散导致 misses 频繁溢出,它的内存占用和延迟就会明显劣于带 sync.RWMutex 的普通 map。别把它当万能并发 map 用。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










