--- tags: [go, golang, go-principle, sync-map] create time: 2026-06-07 15:20 --- # Sync.Map 底层原理 ## 概述 本文深入解析 `sync.Map` 的三表架构(read / dirty / amended),以及 Store / Load / Delete / Range 四个核心操作的源码实现。与原生 map 不同,sync.Map 采用空间换时间的策略,在特定场景下(读多写少)性能远超加锁的原生 map。 > [!question] ❓ 思考 > 为什么 sync.Map 要设计 read 和 dirty 两个 map?如果一个 key 同时存在于两者中,它们指向的是同一个 value 吗?expunged 状态存在的意义是什么? ## 正文 ### 一、Sync.Map 的整体架构 ```mermaid graph TB subgraph Map["sync.Map"] mu["mu: Mutex
保护 dirty"] read["read: atomic.Value
readOnly{m, amended}"] dirty["dirty: map[key]*entry
需加锁访问"] misses["misses: int
未命中计数器"] end subgraph ReadOnly rm["m: map[key]*entry"] am["amended: bool"] end subgraph Entry["entry.p 三种状态"] nil_s["nil = 软删除
key 只存在于 read"] exp["expunged = 假删除
key 在 read 但不在 dirty"] val["正常值 = p → &value"] end read --> ReadOnly ReadOnly --> rm ReadOnly --> am dirty --> Entry mu -.保护.-> dirty misses -.计数.-> dirty style Map fill:#e3f2fd style read fill:#e8f5e9 style dirty fill:#fff9c4 ``` #### 核心字段说明 | 字段 | 类型 | 作用 | |------|------|------| | `mu` | `Mutex` | 保护 `dirty` map 的互斥锁 | | `read` | `atomic.Value` | 原子读取的只读 map,类型为 `readOnly` | | `dirty` | `map[interface{}]*entry` | 需要加锁的可变 map | | `misses` | `int` | read 未命中次数,达到 dirty 长度时触发提升 | > [!tip] 💡 理解要点 > read 中的 `*entry` 和 dirty 中的 `*entry` 可能指向**同一个 entry 对象**。所以通过 read 修改 value 后,dirty 也能读到新值。 ### 二、Entry 的三种状态 这是 sync.Map 最精妙的设计之一——用 `entry.p` 的不同值表示不同的删除状态: ```go type entry struct { p unsafe.Pointer // 指向真实 value } ``` | 状态 | p 的值 | 含义 | dirty 状态 | |------|--------|------|-----------| | 正常 | `&value` | 有效数据 | 包含该 key | | 软删除 | `nil` | key 被删除,但未重建 dirty | dirty 可能为 nil 或包含该 key | | 假删除 | `expunged` | key 在 read 中但不存在于 dirty | 不包含该 key,且 dirty 中有其他 read 没有的 key | > [!note] 📝 为什么需要 expunged? > 如果只有 nil 表示删除,当从 read 读取到 nil 状态的 entry 时,无法判断 dirty 中是否也有这个 key。有了 expunged 后:nil 表示"不用查 dirty",expunged 表示"必须查 dirty",从而避免不必要的加锁。 ### 三、核心操作源码解析 #### 3.1 Store:先试无锁,失败再加锁 ```go func (m *Map) Store(key, value any) { // 第一步:无锁尝试——直接 CAS 更新 read 中的 entry read, _ := m.read.Load().(readOnly) if e, ok := read.m[key]; ok && e.tryStore(&value) { return // 成功!全程无锁 } // 第二步:加锁,处理各种边界情况 m.mu.Lock() read, _ = m.read.Load().(readOnly) // 双重检查 if e, ok := read.m[key]; ok { if e.unexpungeLocked() { // expunged → nil m.dirty[key] = e // 加入 dirty } e.storeLocked(&value) // 更新 value } else if e, ok := m.dirty[key]; ok { e.storeLocked(&value) // dirty 中已有,直接更新 } else { // 全新 key if !read.amended { m.dirtyLocked() // 根据 read 重建 dirty m.read.Store(readOnly{m: read.m, amended: true}) } m.dirty[key] = newEntry(value) // 写入 dirty } m.mu.Unlock() } ``` 流程概括: ```mermaid flowchart TD Start["Store key, value"] --> LockFree{"read 中存在且非 expunged?"} LockFree -->|是| CAS["CAS 原子更新 entry.p"] CAS --> Done["完成,无锁"] LockFree -->|否| Lock["加锁 mu"] Lock --> ReadExist{"read 中存在?"} ReadExist -->|是| HandleExpunged{"p == expunged?"} HandleExpunged -->|是| AddToDirty["expunged→nil, 加入 dirty"] HandleExpunged -->|否| UpdateRead["更新 read 中的 entry"] ReadExist -->|否| DirtyExist{"dirty 中存在?"} DirtyExist -->|是| UpdateDirty["更新 dirty 中的 entry"] DirtyExist -->|否| NewKey{"amended 标记?"} NewKey -->|false| RebuildDirty["重建 dirty + 设 amended=true"] NewKey -->|true| WriteDirty["直接写入 dirty"] AddToDirty --> Unlock["解锁"] UpdateRead --> Unlock UpdateDirty --> Unlock RebuildDirty --> Unlock WriteDirty --> Unlock Unlock --> Done style CAS fill:#e8f5e9 style Lock fill:#fff3e0 ``` #### 3.2 Load:优先无锁读 ```go func (m *Map) Load(key interface{}) (value interface{}, ok bool) { read, _ := m.read.Load().(readOnly) if e, ok := read.m[key]; ok && e != expunged { return e.load() // 无锁命中! } // 未命中或需要查 dirty m.mu.Lock() read, _ = m.read.Load().(readOnly) // 双重检查 if e, ok := read.m[key]; ok && e != expunged { return e.load() } else if ok, e := m.dirty[key]; ok { m.missLocked() // misses++,可能触发 dirty 提升 return e.load() } m.mu.Unlock() return nil, false } ``` 当 `misses >= len(dirty)` 时触发**dirty 提升**: ```go func (m *Map) missLocked() { m.misses++ if m.misses >= len(m.dirty) { m.read.Store(readOnly{m: m.dirty}) // dirty → read m.dirty = nil // dirty 被 GC m.misses = 0 } } ``` #### 3.3 Delete:延迟删除 vs 立即删除 ```go func (m *Map) Delete(key interface{}) { read, _ := m.read.Load().(readOnly) if e, ok := read.m[key]; ok { e.delete() // 软删除:p → nil,不实际移除 return } // key 不在 read 中,直接从 dirty 删除 m.mu.Lock() read, _ = m.read.Load().(readOnly) if e, ok := read.m[key]; ok { e.delete() } else if _, ok = m.dirty[key]; ok { delete(m.dirty, key) // 真正的删除 } m.mu.Unlock() } ``` > [!warning] ⚠️ 注意 > 从 read 中删除是**延迟删除**(只改标记),从 dirty 中删除是**立即删除**(调用 `delete()`)。被软删除的 key 只有在 dirty 提升为 read 时才会真正释放。 #### 3.4 Range:遍历前先确保一致性 ```go func (m *Map) Range(f func(key, value interface{}) bool) { read, _ := m.read.Load().(readOnly) if read.amended { m.mu.Lock() // 双重检查 read, _ = m.read.Load().(readOnly) if read.amended { read = readOnly{m: m.dirty} // 直接用 dirty 遍历 m.read.Store(read) m.dirty = nil m.misses = 0 } m.mu.Unlock() } for k, e := range read.m { v, ok := e.load() if !ok { continue } if !f(k, v) { break } } } ``` Range 会先将 dirty 提升到 read,然后对 read 进行无锁遍历,避免了遍历时加锁的性能损失。 ### 四、P 状态流转示例 通过一个具体例子来理解 entry.p 的状态变化: ```mermaid flowchart LR S1["初始: Store key1, key2 → dirty 有数据, read 为空"] S2["Load 两次 → misses=2=len(dirty) → dirty 提升为 read"] S3["Delete key1 → read 中 key1.p=nil"] S4["Store key3 → dirty 重建: nil→expunged, copy 剩余"] S5["Store key1 新值 → expunged→nil, 加入 dirty"] style S1 fill:#e3f2fd style S2 fill:#fff9c4 style S3 fill:#fff3e0 style S4 fill:#fce4ec style S5 fill:#e8f5e9 ``` ### 五、适用场景分析 sync.Map 并非在所有场景都优于原生 map + mutex: | 场景 | 推荐方案 | 原因 | |------|---------|------| | 读多写少 | `sync.Map` | 读操作几乎无锁 | | 写多读少 | `map + mutex` | sync.Map 的锁竞争成本高 | | 读写均衡 | `map + RWMutex` | 读写分离更灵活 | | 每个 key 独立热点 | `分片 map + mutex` | 降低锁粒度 | > [!info] ℹ️ 补充 > sync.Map 在 Go 1.9 引入,最初设计用于反射等低频访问场景。Go 1.10+ 对其进行了大量优化,才逐渐成为通用并发 map 的首选方案。 ## 小结 - sync.Map 通过 read(无锁)+ dirty(加锁)双表结构实现读写分离 - entry.p 的三种状态(正常/nil/expunged)精确控制是否需要加锁 - Store 优先无锁 CAS,Load 优先无锁读,最大限度减少锁竞争 - 适合读多写少的场景;读写均衡时用 map + RWMutex 更合适 ## 关联笔记 - [[hzh/GolangStar/Go语言进阶/Sync]] — Sync 包的基础用法 - [[hzh/GolangStar/Go语言基础/Go语言Map]] — Map 的基础用法 - [[hzh/GolangStar/Go面试题库/Sync面试题]] — Sync 同步相关面试题