Go sync.Map 底层实现
一、sync.Map 是什么?
一句话:
Go 内置的、高并发安全的、读超级快、写较少的高性能 map
为什么要用 sync.Map?
- 普通 map 非并发安全,多 goroutine 读写会 panic
- map + sync.Mutex 大并发读很慢(全局锁)
- sync.Map 专门优化:读多写少场景
核心设计
空间换时间 + 读写分离 + 双 map
- read map:只读,无锁,超快
- dirty map:可写,加锁
- miss 计数器:read 没命中太多,就把 dirty 升级为 read
口诀:读走 read、写走 dirty、miss 够了就晋升
二、sync.Map 核心源码结构
文件:sync/map.go
1. 最外层:sync.Map 结构体
type Map struct {
mu Mutex // 互斥锁:只在操作 dirty、删除、晋升时用
// read: 只读 map,无锁访问(存部分数据)
read atomic.Pointer[readOnly]
// dirty: 最新数据 map,包含所有数据(需要加锁)
dirty map[any]*entry
// 计数器:read 没命中次数,达到阈值就把 dirty 升级为 read
misses int
}
2. readOnly(read map 里存的结构)
type readOnly struct {
m map[any]*entry // 实际的只读 map
amended bool // 标记:dirty 里是否有 read 没有的新数据
}
3. entry(真正存值的地方,原子操作)
type entry struct {
// 原子指针:存 value
// 状态:正常值 / 被删除(expunged) / nil
p atomic.Pointer[any]
}
三、核心流程
读流程(最快)
- 先从 read map 读(无锁)
- 读到 → 直接返回
- 没读到 → 加锁再查一遍
- 还没读到 → 返回不存在
- miss 次数够了 → dirty 晋升为 read
写 / 更新流程
- 先去 read map 看有没有
- 有 → 原子更新(无锁)
- 没有 → 加锁,写到 dirty map
- 标记 amended=true(read 和 dirty 不一致)
删除流程
- 不是真删!
- read 里:标记为删除
- dirty 里:真正删除
四、sync.Map 核心方法源码
1. Load(读)
// Load 读取 key 对应的值
func (m *Map) Load(key any) (value any, ok bool) {
// 第一步:原子读 read map(无锁!)
read := m.read.Load()
// 从 read 里找 key
e, ok := read.m[key]
// 如果 read 里没有,且 dirty 里有新数据(amended=true)
if !ok && read.amended {
// 加锁(慢路径)
m.mu.Lock()
// 双重检查(防止并发变化)
read = m.read.Load()
e, ok = read.m[key]
// 还是没有,且 dirty 有新数据
if !ok && read.amended {
// 从 dirty 里读
e, ok = m.dirty[key]
// miss 计数 +1,达到阈值就晋升 dirty → read
m.missLocked()
}
m.mu.Unlock()
}
// 没找到
if !ok {
return nil, false
}
// 原子获取值
return e.load()
}
2. Store(写 / 更新)
func (m *Map) Store(key, value any) {
// 第一步:尝试 read map 无锁更新
read := m.read.Load()
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return // 更新成功,直接返回
}
// 没更新成功 → 加锁,走 dirty
m.mu.Lock()
// 再次双重检查
read = m.read.Load()
if e, ok := read.m[key]; ok {
// 存在 → 原子更新
e.store(&value)
} else if e, ok := m.dirty[key]; ok {
// dirty 里存在 → 更新
e.store(&value)
} else {
// 全新 key → 加入 dirty
if m.dirty == nil {
// dirty 为空,先从 read 复制数据
m.dirtyDirty()
}
// 新key存入dirty
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
3. missLocked(dirty 晋升 read —— 灵魂机制)
func (m *Map) missLocked() {
m.misses++
// 当 miss 次数 == dirty 元素数量时
if m.misses < len(m.dirty) {
return
}
// 直接把 dirty 变成新的 read!
m.read.Store(&readOnly{m: m.dirty})
// 清空 dirty
m.dirty = nil
m.misses = 0
}
五、sync.Map 完整流程图
1. 读(Load)流程图
2. 写(Store)流程图
六、总结
sync.Map 三大核心
- read map:无锁、只读、超快
- dirty map:加锁、存最新数据
- miss 晋升:read 命中太低 → dirty 直接变 read
优点
- 读超级快(无锁)
- 高并发安全
- 读多写少场景性能碾压 map+mutex
缺点
- 结构复杂
- 写、删除都相对慢
- 不适合频繁写入
七、面试(懂了说清楚就行)
1. sync.Map 为什么快?
读无锁、双 map 分离、miss 晋升机制,读多写少场景最优。
2. read 和 dirty 什么关系?
- read 是快照
- dirty 是最新数据
- miss 够了 dirty 直接升级成 read
3. 为什么不直接用 map+mutex?
全局锁在高并发读会严重阻塞,sync.Map 读完全无锁。
4. entry 是什么?
存值的原子结构,保证原子读写,不产生竞态。
5. 什么场景用 sync.Map?
读多写少、全局缓存、并发配置、长生命周期 key。