sync.Map
约 2185 字大约 7 分钟
2025-12-21
sync.Map 的设计目标
sync.Map 是标准库提供的并发安全 map,针对两种场景做了专门优化:
- 读多写少(如只增长的缓存):key 写入一次后被大量读取
- key 分离:多个 goroutine 读写互不重叠的 key 集合
在这两种场景下,sync.Map 相比 map + sync.RWMutex 能显著减少锁竞争,因为大部分操作可以走无锁的快速路径。
底层结构
Map sync/map.go
type Map struct {
mu Mutex
// read 包含 map 中可安全并发访问的部分(持有或不持有 mu 均可)。
// read 字段本身始终可安全 Load,但 Store 时必须持有 mu。
// read 中存储的 entry 可以在不持有 mu 的情况下通过 CAS 并发更新,
// 但更新一个已被标记为 expunged 的 entry 前,必须先将其拷贝到 dirty 并取消 expunged 标记。
read atomic.Pointer[readOnly]
// dirty 包含 map 中需要持有 mu 才能访问的部分。
// 为了保证 dirty 能快速提升为 read,dirty 同时包含 read 中所有未被 expunged 的 entry。
// 被 expunged 的 entry 不会出现在 dirty 中。
// 向一个已 expunged 的 entry 存储新值前,必须先将其取消 expunged 并加入 dirty。
// 当 dirty 为 nil 时,下一次写入会通过浅拷贝 read(跳过 stale entry)来初始化 dirty。
dirty map[any]*entry
// misses 记录自 read 上次更新以来,需要加锁才能判断 key 是否存在的 Load 次数。
// 当 misses 累积到足以覆盖拷贝 dirty 的开销时,dirty 会被提升为 read。
misses int
}readOnly
readOnly 是一个不可变结构体,通过 atomic.Pointer 存储在 Map.read 中:
type readOnly struct {
m map[any]*entry
amended bool // 为 true 表示 dirty 中包含 read.m 中不存在的 key
}m:无锁可读的 mapamended:标志位,表示 dirty 中是否有新增的 key。为false时,所有数据都在read.m中,读操作无需加锁
entry
每个 key 对应一个 entry,它是 read 和 dirty 之间的共享指针:
var expunged = new(any)
type entry struct {
// p 指向该 entry 存储的 interface{} 值。
//
// p == nil → entry 已删除,且 dirty == nil 或 dirty[key] == e
// p == expunged → entry 已删除,且 dirty != nil,dirty 中没有该 key
// 其他 → entry 有效,存在于 read.m[key],若 dirty != nil 也存在于 dirty[key]
p atomic.Pointer[any]
}entry.p 的三种状态是 sync.Map 的核心设计:
entry.p 值 | 含义 | dirty 中的状态 |
|---|---|---|
| 正常指针 | entry 有效 | 若 entry 在 read.m 中且 dirty != nil,dirty[key] 指向同一 entry;也可能仅存在于 dirty 中(新增未提升的 key) |
nil | entry 已软删除 | dirty == nil,或 dirty[key] 指向同一个 entry |
expunged | entry 已删除并在重建 dirty 时被清理 | dirty 中没有该 entry |
nil 和 expunged 的区分是关键:重建新的 dirty 时(dirtyLocked),遍历 read.m 会把 p == nil 的 entry CAS 为 expunged,这些 entry 不会被复制进新的 dirty,从而避免 dirty 中携带无用的已删除条目。
key1、key2 的 entry 被 read 和 dirty 共享(同一个指针),因此对 entry 的 CAS 更新对两边同时可见。key3 仅存在于 dirty(amended = true),key4 已被 expunged,不在 dirty 中。
核心操作
Load(读取)
func (m *Map) Load(key any) (value any, ok bool) {
read := m.loadReadOnly()
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
// 双检查:加锁期间 dirty 可能已被提升
read = m.loadReadOnly()
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// 无论是否命中,都记录一次 miss
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
return e.load()
}快速路径:直接从 read.m 查找,无需加锁。仅当 read 中找不到且 amended == true(dirty 中可能有新 key)时才加锁查 dirty。
进入慢路径后会调用 missLocked(),累积足够的 miss 后触发提升。
missLocked(dirty 提升)
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
m.read.Store(&readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}当 misses >= len(dirty) 时,将 dirty 直接提升为 read(O(1) 指针赋值),然后将 dirty 置空。提升后所有 key 都在 read 中,后续读操作恢复无锁快速路径。
提升阈值设为 len(dirty) 的含义是一种摊还成本论证:当累计 miss 次数达到 len(dirty) 时,这些 miss 付出的加锁与查找开销在数量级上已经接近一次完整拷贝 dirty 的代价,此时将 dirty 一次性提升为 read、让后续读恢复无锁路径是划算的。
Store / Swap(写入)
Store 内部调用 Swap:
func (m *Map) Swap(key, value any) (previous any, loaded bool) {
read := m.loadReadOnly()
if e, ok := read.m[key]; ok {
// 快速路径:key 在 read 中,CAS 替换值
if v, ok := e.trySwap(&value); ok {
if v == nil {
return nil, false
}
return *v, true
}
}
m.mu.Lock()
read = m.loadReadOnly()
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() {
// entry 之前被 expunged,需要重新加入 dirty
m.dirty[key] = e
}
if v := e.swapLocked(&value); v != nil {
loaded = true
previous = *v
}
} else if e, ok := m.dirty[key]; ok {
if v := e.swapLocked(&value); v != nil {
loaded = true
previous = *v
}
} else {
if !read.amended {
// 自上次提升后,首次写入 read 中不存在的 key:
// 需要初始化 dirty(若为 nil)并将 read 标记为 amended
m.dirtyLocked()
m.read.Store(&readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
return previous, loaded
}写入分三种情况:
- key 在 read 中且未 expunged:直接 CAS 更新 entry,无需加锁(因为 read 和 dirty 共享同一个 entry 指针)
- key 在 read 中但已 expunged:加锁,取消 expunged,将 entry 加回 dirty,再更新值
- key 仅在 dirty 中或不存在:加锁操作 dirty
dirtyLocked(dirty 初始化)
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
read := m.loadReadOnly()
m.dirty = make(map[any]*entry, len(read.m))
for k, e := range read.m {
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
func (e *entry) tryExpungeLocked() (isExpunged bool) {
p := e.p.Load()
for p == nil {
if e.p.CompareAndSwap(nil, expunged) {
return true
}
p = e.p.Load()
}
return p == expunged
}当 dirty 为 nil 时(提升后或初始状态),首次写入新 key 会触发 dirty 初始化:遍历 read.m,将所有 p == nil(已删除)的 entry 标记为 expunged 并跳过,只将有效 entry 拷贝到 dirty 中。这保证了 dirty 不携带无用条目。
Delete / LoadAndDelete(删除)
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
read := m.loadReadOnly()
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
read = m.loadReadOnly()
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
delete(m.dirty, key)
m.missLocked()
}
m.mu.Unlock()
}
if ok {
return e.delete()
}
return nil, false
}
func (e *entry) delete() (value any, ok bool) {
for {
p := e.p.Load()
if p == nil || p == expunged {
return nil, false
}
if e.p.CompareAndSwap(p, nil) {
return *p, true
}
}
}两条路径最终都汇合到 e.delete(),由它通过 CAS 把 entry.p 置为 nil 并返回旧值,差异只在前置处理:
- 若 key 在 read 中:无需加锁,直接
e.delete()做软删除(把entry.pCAS 为 nil)。entry 本身仍留在 read.m 中;若此时dirty != nil,dirty[key] 指向的是同一个 entry,因此通过 dirty 读取时也能看到这次变更。下次dirtyLocked重建 dirty 时,该 entry 会被标记为expunged并不再进入新的 dirty - 若 key 仅在 dirty 中:先加锁,从 dirty 的哈希表中摘除该 key(
delete(m.dirty, key),避免 dirty 累积已删条目被一并提升到 read),释放锁后再走公共出口e.delete()原子取回旧值
Range(遍历)
func (m *Map) Range(f func(key, value any) bool) {
read := m.loadReadOnly()
if read.amended {
m.mu.Lock()
read = m.loadReadOnly()
if read.amended {
// 立即将 dirty 提升为 read
read = readOnly{m: m.dirty}
copyRead := read
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 本身已是 O(N) 操作,因此直接将 dirty 提升为 read(摊还了拷贝成本),然后遍历 read.m。遍历过程中跳过 p == nil 或 p == expunged 的 entry。
dirty 提升与重建周期
适用场景与局限
适用场景
| 场景 | 原因 |
|---|---|
| 读多写少的缓存 | 大部分 Load 走无锁快速路径 |
| 各 goroutine 操作不同 key | 写操作不竞争同一个 entry,减少锁冲突 |
| 配置热加载(一次写入,长期读取) | 新 key 先入 dirty,经过一次提升后 entry 长驻 read,后续读全部走无锁路径 |
不适用场景
| 场景 | 原因 |
|---|---|
| 写多读少 | 频繁写新 key 导致反复重建 dirty(O(N) 拷贝) |
| 需要遍历一致性快照 | Range 不保证一致性快照 |
| 需要类型安全 | key/value 均为 any,无编译期检查 |
