8.9 KiB
tags, create time
| tags | 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 的整体架构
graph TB
subgraph Map["sync.Map"]
mu["mu: Mutex<br/>保护 dirty"]
read["read: atomic.Value<br/>readOnly{m, amended}"]
dirty["dirty: map[key]*entry<br/>需加锁访问"]
misses["misses: int<br/>未命中计数器"]
end
subgraph ReadOnly
rm["m: map[key]*entry"]
am["amended: bool"]
end
subgraph Entry["entry.p 三种状态"]
nil_s["nil = 软删除<br/>key 只存在于 read"]
exp["expunged = 假删除<br/>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 的不同值表示不同的删除状态:
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:先试无锁,失败再加锁
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()
}
流程概括:
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:优先无锁读
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 提升:
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 立即删除
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:遍历前先确保一致性
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 的状态变化:
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 同步相关面试题