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