(五)Sync.Map

9 阅读3分钟

基础概念

什么是并发安全?

当一个数据结构被多个goroutine同时访问时,如果不会出现数据不一致、数据损坏或数据竞争问题,就称为并发安全

Map 并发安全吗?

结论:不安全。

  • 竞态条件:Map 内部有一个标志位。任何写操作开始前会检查该位,并将其设为“正在写入”。如果此时发现已经被设为“正在写入”,直接抛出 fatal("concurrent map writes")。
  • 解决方案
    1. sync.Mutex:最通用,写锁保护。
    2. sync.RWMutex:读多写少场景,性能更好。
    3. sync.Map:官方提供的并发版 Map,适合“读多写少”且“键不怎么变化”的特定场景。
    4. 分段锁:将一个大 Map 拆分成多个小 Map,减少锁竞争(类似 Java 的ConcurrentHashMap 原理)

什么是sync.Map?

sync.Map是Go标准库提供的一种并发安全的Map实现,它属于sync包,专门为高并发场景设计。

核心特点

  • 不需要手动加锁
  • 支持多个goroutine同时读写
  • 读写分离优化,读操作无锁
  • 内部使用复杂的数据结构实现高性能

image.png

核心架构详解

结构理解

在最新的 Go 版本中,sync.Map 的底层不再是简单的 read/dirty 两张表,而是进化成了一种高性能的哈希树结构

而哈希树是基于字典树trie的,我们先看下字典树是什么

字典树(Trie)是一种通过前缀快速匹配的结构。以空间换时间,就像在字典里查“Apple”,你会先找 A,再找 p。它通过空间换时间,让查找速度只与字符串长度有关,而与字典大小无关。

image.png 在最新版 Go 中,sync.Map 借鉴了这种思想。它将 Key 的 哈希值 当作字符串,构建了一棵高性能的哈希树。

  • 分层规则:哈希值每 4 位 为一层。

  • 分支结构:4 位二进制对应 16 种组合,所以每个节点有 16 个孩子

  • 树的深度:如果是 64 位哈希值,理论最大深度为 64/4=1664 / 4 = 16 层。

再小树太深、Load 会慢很多;再大节点太大、收益几乎可以忽略

image.png

工作原理

查找流程
  1. 计算hash值
  2. 每四位进行匹配槽位
  3. 看类型
    1. 是nil,说明key不存在
    2. 是indirect,继续找
    3. 是entry,比对key,匹配则返回,否则沿着overflow找
添加流程
  1. 跟查找流程一样找插入/更新点
  2. 锁住当前层 indirect,再读一次槽和 i.dead
    1. 若槽已被改成 indirect 或节点已死,就解锁并从头重试
  3. 持锁后
    1. 槽里已有 entry,更新
    2. 槽是空的,新增
    3. 槽里有 entry 但 key 不在链里,需要扩展

image.png

扩展流程

同一个槽里已经有一个 entry(oldEntry),现在要插入的 key 的 hash 前缀和它一样,但 key 不同,就要在这个槽上建一层(或多层)indirect,把两个 key 按 hash 的更低位分到不同槽

  1. 两个 key 的完整 hash 相同(真冲突),另一个挂到 overflow 上
  2. 两个 key 的 hash 不同(可以在更低位区分),用 hash 的更低位的 4 位,让 old 和 new 落在不同槽。

image.png

最佳实践

不要默认用 sync.Map

多数代码应该用普通 map + 自己的锁(或其它同步**)**,这样类型更安全、也方便维护和 map 一起的其它变量

适合的两种场景

场景说明典型例子
一写多读同一个 key 只写一次,之后大量读只增不减的缓存、配置表
多 goroutine 操作不同 key各 goroutine 访问的 key 集合几乎不重叠每个 goroutine 维护自己的 key

image.png