Golang sync.Map
sync.Map 是 Go 语言标准库 sync 包中提供的一个并发安全的 map。它并非为了替代 map + sync.RWMutex 这种通用模式,而是针对**“读多写少”**的特定场景进行了深度优化。
一、 核心设计思想
sync.Map 的设计哲学是读写分离和空间换时间,其最终目的是:
让读操作尽可能快,甚至做到在大部分情况下无锁(lock-free)。
为了实现这个目标,它牺牲了写操作的性能,并使用了比普通 map 更复杂的内部结构。
二、 核心数据结构
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)
当 Load 操作在 read 中未命中,且 misses 计数器增长到等于 dirty map 的大小时,会触发这个过程:
-
加锁
mu。 -
将
dirtymap 中的所有数据(包括read中已有的但被更新的数据)合并到一个新的 map 中,这个新 map 将成为新的readOnly.m。 -
通过原子操作,将
sync.Map的read指针指向这个包含全新数据的readOnly结构。 -
将
dirtymap 清空(或设为nil),并将misses计数器重置为0。 -
解锁
mu。
五、 总结与对比
| 特性 | 手动加锁 (map + sync.RWMutex) |
sync.Map |
|---|---|---|
| 核心机制 | 单一锁(读写锁)保护整个 map | 两个 map(read/dirty),读大多无锁,写加锁 |
| 最佳使用场景 | 写多读少,或读写均衡的场景;或map创建后不经常修改的场景 | 读多写少,特别是“一次写入,多次读取”的场景 (如缓存) |
| 性能特点 | 读和写操作都会有锁的开销,高并发读写时锁竞争激烈 | 键稳定存在时,读操作极快(接近原子操作);写操作相对较慢 |
| 锁竞争与扩展性 | 整个 map 共享一把锁,高并发下所有操作都可能互相阻塞,扩展性受限 | 读操作无锁竞争,写操作有锁,但读写分离,显著降低了读操作的竞争,在高并发读场景下扩展性好 |
| API 与易用性 | 开发者需手动管理锁,但 API 就是普通 map 的 API | 封装好的 API (Load, Store, Delete, Range),无需手动管锁,但 API 功能受限(如无法直接获取长度) |
| 类型安全 | 编译时类型安全(map[string]int) |
运行时类型断言(key, value 都是 any 类型) |