sync.map 仅适用于读多写少、key 基本不重复写入的场景;load 无锁因先原子读 read 字段查 readonly.m,amended=true 时才加锁查 dirty;store 双重检查以优先无锁更新 entry;delete 是逻辑删除,物理清理延至 dirty 晋升;range 不保证强一致性,只确保至少遍历一次。

sync.Map 不是通用并发 map 替代品,它只在读多写少、key 基本不重复写入的场景下才有明显优势;盲目用它替换 map + sync.RWMutex 反而可能拖慢性能。
为什么 sync.Map 的 Load 不加锁却能安全?
因为它的读路径完全绕开了互斥锁:核心是 read 字段,一个 atomic.Pointer[readOnly]。每次 Load 都先原子读取这个指针,再查其指向的 readOnly.m(底层仍是普通 map)。只要没触发晋升(即 dirty 未提升为新 read),整个过程无锁、无竞争。
但要注意:readOnly.amended == true 时,说明 dirty 里有 read 没覆盖的 key,此时 Load 会退到加锁的 dirty 查找——这正是“读多写少”才能稳住性能的关键前提。
- 命中
read:零开销,纯原子读 + map 查表 - 未命中且
amended == false:直接返回 not found,不碰锁 - 未命中且
amended == true:必须获取mu锁,查dirty
Store 操作为何要双重检查?
Store 的目标是:新 key 写入 dirty,已存在 key 尽量更新 read 中的 *entry(避免锁)。所以它分两步走:
先尝试无锁更新 read 中对应 key 的 *entry.p(用 atomic.CompareAndSwapPointer);失败则说明 key 不在 read 中,或已被删除,此时才加锁,检查 dirty 并决定是否插入或升级。
- 已存在且未被删:
read中直接 CAS 更新 value,快 - 不存在或已标记为
expunged:进锁,写dirty,并可能触发misses++ - 若
dirty == nil(刚晋升完),需从read复制一份非expunged的子集过去
Delete 是真删还是假删?
Delete 是逻辑删除:它只把对应 *entry.p 设为 nil(不是 expunged)。这意味着:
-
Load看到nil就返回 not found,行为正确 -
Store看到nil会尝试 CAS 替换,成功即“复活”该 key - 真正物理清理发生在
dirty晋升为新read时:遍历dirty,跳过所有nil和expunged条目
这种延迟回收减少了写操作的锁持有时间,但代价是内存不会立刻释放——如果你高频增删同一 key,sync.Map 内存占用会缓慢爬升,直到下次晋升。
Range 遍历时看到的是哪一刻的数据?
Range 不加锁,也不做快照。它先原子读一次 read,遍历其全部 key;再加锁读 dirty,只遍历那些 read 里没有的 key。这意味着:
- 遍历期间新
Store到dirty的 key 会被包含(如果read没它) - 遍历期间发生的
Delete可能被漏掉(因Range看的是 entry 当前状态,而Delete只设nil) - 同一个 key 绝对不会出现两次:
read有的,dirty就跳过
所以 Range 不保证强一致性,只保证“至少看到一次”——适合监控、导出等容忍短暂偏差的场景,不适合做原子性校验。
最易被忽略的一点:sync.Map 的 zero value 是可用的,无需显式初始化;但一旦你把它作为结构体字段嵌入,又没导出方法控制访问,就很容易在无意中破坏其内部状态(比如并发调用未同步的 Load 和 Store)。它不是“设了就忘”的容器,而是需要你对其读写比例、key 生命周期有明确预判的工具。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











