Golang sync.Map
sync.Map 是 Go 语言标准库 sync 包中提供的一个并发安全的 map。它并非为了替代 map + sync.RWMutex 这种通用模式,而是针对**“读多写少”**的特定场景进行了深度优化。
一、 核心设计思想
sync.Map 的设计哲学是读写分离和空间换时间,其最终目的是:
让读操作尽可能快,可以做到在大部分情况下无锁(lock-free)。
二、 核心数据结构
sync.Map 的内部实现主要围绕两个核心的 map 结构和一个锁:
-
read(只读 map)-
这是一个
atomic.Pointer,指向一个内部的readOnly结构体。readOnly结构体里包含一个普通的 Gomap。 -
这个
readmap 存储了 map 中被认为是 “稳定” 的数据 。 -
对
readmap 的访问是原子操作,因此读取是并发安全的,并且不需要加锁。这是sync.Map高性能读取的关键。
-
-
dirty(可写 map)-
这是一个普通的 Go
map(map[any]any)。 -
它存储了最近新增或被修改的键值对。可以把它看作是新数据的“暂存区”或“缓存”。
-
对
dirtymap 的所有访问都必须由一个互斥锁mu来保护。
-
-
mu(互斥锁sync.Mutex)- 这个锁只用于保护
dirtymap 的并发访问。它不保护readmap。
- 这个锁只用于保护
-
misses(未命中计数器)- 用于记录
Load操作在readmap 中未命中、不得不去查询dirtymap 的次数。当misses的数量达到一定阈值(等于dirtymap 的长度)时,就会触发一次数据从dirty到read的迁移。
- 用于记录
三、 结构图
graph TD
subgraph sm [sync.Map 实例]
mu["mu (sync.Mutex)"]
read_ptr["read (atomic.Pointer)"]
dirty_map["dirty (map[any]any)"]
misses["misses (int)"]
end
subgraph ro [readOnly 结构]
m["m (map[any]*entry)"]
amended["amended (bool)"]
end
subgraph dirty_map_protected["受 mu 锁保护"]
dirty_map
end
mu --- dirty_map_protected
read_ptr -- "原子加载/存储" --> ro
style sm fill:#f9f,stroke:#333,stroke-width:2px
style ro fill:#ccf,stroke:#333,stroke-width:2px
-
sync.Map 实例: 包含一个锁
mu,一个原子指针read,一个普通的 mapdirty和一个计数器misses。 -
mu (sync.Mutex): 它的保护范围仅限于
dirtymap。 -
read (atomic.Pointer): 指向一个
readOnly结构。所有对read的读写都通过原子操作完成,保证并发安全。 -
readOnly 结构: 包含一个 map
m,这是读操作的快速路径。amended标志位表示dirtymap 中是否包含readmap 中没有的数据。 -
dirty map: 是一个常规的 map,存储最新的写入,访问它必须先获取
mu锁。
四、 关键操作流程
A. Load (读取操作) - 性能核心
这是 sync.Map 被优化的主要路径。
-
快速路径(无锁):
-
通过原子操作加载
read指针,获取readOnlymap。 -
在
readOnly.m中查找 key。 -
如果找到了,并且其值不是“已删除”状态,直接返回。这个过程完全无锁,非常快。
-
-
慢速路径(加锁):
-
如果在
read中没找到,说明 key 可能是新写入的,存在于dirty中。 -
加锁
mu,以安全地访问dirtymap。 -
再次检查
readmap(因为在加锁的瞬间,dirty可能已经被提升为了新的readmap),防止数据不一致。 -
在
dirtymap 中查找 key,如果找到则返回。 -
无论是否在
dirty中找到,都将misses计数器加一。 -
检查
misses是否达到了dirtymap 的长度,如果是,则触发一次数据迁移。 -
解锁
mu。
-
B. Store (插入和修改操作)
写入操作通常是慢路径,因为它很可能需要加锁。
-
快速检查:
-
先不加锁,原子加载
readmap,并检查 key 是否存在。 -
如果 key 存在,尝试通过原子操作 (CAS) 直接更新
readmap 中该 key 对应的 entry 的值。如果成功,操作结束。
-
-
慢速路径(加锁):
-
如果上述快速更新不成功(例如 key 不在
read中,或 CAS 操作失败),则加锁mu。 -
再次检查
readmap,因为可能在加锁期间发生了变化。 -
如果 key 在
read中,但被标记为“已删除”,则需要将其“复活”并存入dirty。 -
如果 key 不在
read中,则直接将新的键值对存入dirtymap。 -
如果
dirtymap 为nil,则会根据readmap 的内容创建一个新的dirtymap,并将新数据存入。 -
解锁
mu。
-
C. Delete (删除操作)
删除操作总是慢路径,需要加锁。它会将键值对从 dirty map 中删除,并在 read map 中将对应的条目标记为“已删除”(expunged),这是一个逻辑删除,而不是物理删除。
D. 数据迁移-提升 (dirty -> read)
以下是结合代码分析sync.Map 中 dirty 转换为 read(即 dirty map 提升为 read map)
1. 触发时机
dirty 提升为 read 主要有两种触发情况:
A. 读操作穿透次数过多 (missLocked)
当进行 Load、LoadAndDelete、LoadOrStore 等操作时,如果 read map 中找不到 key,且 read.amended 为 true(表示 dirty 中有 read 没有的数据),就会加锁去 dirty 中查找。
每次需要加锁访问 dirty 时,都会调用 missLocked() 方法。
// src/sync/map.go
func (m *Map) Load(key any) (value any, ok bool) {
// ... (省略 read 查找)
m.mu.Lock()
// ... (双重检查)
if !ok && read.amended {
e, ok = m.dirty[key]
// 记录一次 miss,可能会触发提升
m.missLocked()
}
m.mu.Unlock()
// ...
}
B. 全量遍历 (Range)
当调用 Range 遍历 map 时,如果 read.amended 为 true,说明 dirty 中有新数据。为了保证遍历的完整性且不长时间持有锁,sync.Map 会直接将 dirty 提升为 read。
// src/sync/map.go
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}
m.read.Store(&read)
m.dirty = nil
m.misses = 0
}
m.mu.Unlock()
}
// ...
}
2. 提升流程 (missLocked)
这是最核心的自动提升逻辑。
// src/sync/map.go
func (m *Map) missLocked() {
m.misses++ // 1. 增加 miss 计数
// 2. 判断阈值
// 如果 miss 次数小于 dirty 的长度,则不提升
if m.misses < len(m.dirty) {
return
}
// 3. 执行提升 (Promotion)
// 将 dirty map 封装成 readOnly 结构体,原子替换掉 m.read
// 注意:这里新创建的 readOnly 中 amended 默认为 false
m.read.Store(&readOnly{m: m.dirty})
// 4. 清理状态
m.dirty = nil // dirty 置空
m.misses = 0 // 计数归零
}
3. 流程深度解析
-
计数 (
m.misses++):- 每次在
read中没找到 key 而被迫加锁去dirty找时,misses计数加 1。
- 每次在
-
阈值判断 (
m.misses < len(m.dirty)):- Go 团队设定的阈值是
len(m.dirty)。 - 逻辑: 如果穿透到
dirty的次数已经等于dirty中元素的个数了,说明dirty中的数据被访问得很频繁。此时将dirty提升为read是划算的,因为以后这些 key 的访问就可以直接走无锁的read路径了。
- Go 团队设定的阈值是
-
原子替换 (
m.read.Store(...)):m.read被更新为指向原本的dirtymap。- 新的
readOnly结构体中amended字段为false。这意味着此时read包含了所有数据,dirty中没有额外数据了。
-
置空 Dirty (
m.dirty = nil):dirty变为nil。- 注意: 下一次发生写操作(Store)时,如果发现
dirty为 nil,会触发dirtyLocked,此时会再次把read中的有效数据(非 expunged)浅拷贝回dirty。
这是一个非常敏锐的问题!理解这一点的关键在于:dirty 和 read 虽然是两个不同的 map 结构(map[any]*entry),但它们存储的 value 是指针,指向的是同一个 entry 对象。
简单回答你的问题:
- 是两个 map:
read.m和dirty是两个独立的map对象。 - 数据不会丢失:因为
dirty在创建之初就是read的全量拷贝(除了被标记删除的),并且后续的新增操作都在dirty上。所以dirty包含了read中所有有效的数据,外加新数据。 - 旧 read 被丢弃:替换后,旧的
readmap 对象会被垃圾回收(GC),但它里面的数据(entry)因为被新的read(原来的dirty)引用着,所以不会丢失。
详细分析:为什么 dirty 和 read 虽然是两个不同的 map,但直接替换是安全的?
TLDR
直接替换 m.read 不会丢失数据,因为:
- 包含关系:在提升发生的那一刻,
dirty是read的超集(Superset)。dirty拥有read的所有有效数据,加上read没有的新数据。 - 引用关系:它们共享底层的
entry对象,所以值的更新是同步的。
详细分析:
我们需要看两个关键点:dirty 是怎么创建的 以及 数据是如何共享的。
1. 数据共享机制(指针引用)
sync.Map 的核心数据结构是 entry。read 和 dirty 的 map 定义都是 map[any]*entry。
- Key: 是具体的键值。
- Value: 是一个指针
*entry。
这意味着,如果一个 key 同时存在于 read 和 dirty 中,它们指向的是堆内存中同一个 entry 结构体。
- 当你修改
read中的某个 key 的 value 时,你实际上修改的是那个entry。 - 因为
dirty也指向这个entry,所以dirty也能立即看到这个修改。
2. dirty 的创建(全量继承)
当 dirty 为 nil 且需要写入新 key 时,会触发 dirtyLocked 方法。这个方法负责创建 dirty。
// src/sync/map.go
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
read := m.loadReadOnly()
// 1. 创建一个新的 map,大小等于 read
m.dirty = make(map[any]*entry, len(read.m))
// 2. 把 read 中所有“未被彻底删除”的数据,浅拷贝到 dirty 中
for k, e := range read.m {
if !e.tryExpungeLocked() {
m.dirty[k] = e // 关键:这里复制的是指针!
}
}
}
所以只要 dirty 不为 nil,它就一定包含 read 中所有未被删除的 key。
3. 演变过程图解
假设我们有一个 Map,里面有 key “A”。
阶段 1: 初始状态 (dirty 为 nil)
read: { “A”: entryPtr1 }dirty: nil
阶段 2: 发生 Store(“B”, 2) -> 触发 dirtyLocked
sync.Map发现dirty是 nil,先执行dirtyLocked:- 创建
dirtymap。 - 把 “A” 从
read复制到dirty。此时read和dirty都指向entryPtr1。
- 创建
- 然后把 “B” 写入
dirty。 - 状态:
read: { “A”: entryPtr1 } (amended=true)dirty: { “A”: entryPtr1, “B”: entryPtr2 }
阶段 3: 发生 Store(“A”, 3) -> 更新已有 Key
- 直接修改
entryPtr1的内容。 - 因为
read和dirty都指向entryPtr1,所以它们都看到了 “A” 变成了 3。
阶段 4: 发生 missLocked -> 提升 dirty 为 read
- 执行
m.read.Store(&readOnly{m: m.dirty})。 - 新状态:
read: { “A”: entryPtr1, “B”: entryPtr2 } (原本的 dirty)dirty: nil
- 旧 read: { “A”: entryPtr1 }。这个 map 对象被丢弃,等待 GC。