go从零单排之sync.map

0 阅读3分钟

Go sync.Map 底层实现


一、sync.Map 是什么?

一句话:

Go 内置的、高并发安全的、读超级快、写较少的高性能 map

为什么要用 sync.Map?

  • 普通 map 非并发安全,多 goroutine 读写会 panic
  • map + sync.Mutex 大并发读很慢(全局锁)
  • sync.Map 专门优化:读多写少场景

核心设计

空间换时间 + 读写分离 + 双 map

  1. read map:只读,无锁,超快
  1. dirty map:可写,加锁
  1. miss 计数器:read 没命中太多,就把 dirty 升级为 read

口诀:读走 read、写走 dirty、miss 够了就晋升


二、sync.Map 核心源码结构

文件:sync/map.go

1. 最外层:sync.Map 结构体

type Map struct {
        mu Mutex          // 互斥锁:只在操作 dirty、删除、晋升时用
        // read: 只读 map,无锁访问(存部分数据)
        read atomic.Pointer[readOnly]
        // dirty: 最新数据 map,包含所有数据(需要加锁)
        dirty map[any]*entry
        // 计数器:read 没命中次数,达到阈值就把 dirty 升级为 read
        misses int
}

2. readOnly(read map 里存的结构)

type readOnly struct {
        m       map[any]*entry // 实际的只读 map
        amended bool           // 标记:dirty 里是否有 read 没有的新数据
}

3. entry(真正存值的地方,原子操作)

type entry struct {
        // 原子指针:存 value
        // 状态:正常值 / 被删除(expunged) / nil
        p atomic.Pointer[any]
}

三、核心流程

读流程(最快)

  1. 先从 read map 读(无锁)
  1. 读到 → 直接返回
  1. 没读到 → 加锁再查一遍
  1. 还没读到 → 返回不存在
  1. miss 次数够了 → dirty 晋升为 read

写 / 更新流程

  1. 先去 read map 看有没有
  1. 有 → 原子更新(无锁)
  1. 没有 → 加锁,写到 dirty map
  1. 标记 amended=true(read 和 dirty 不一致)

删除流程

  • 不是真删!
  • read 里:标记为删除
  • dirty 里:真正删除

四、sync.Map 核心方法源码

1. Load(读)

// Load 读取 key 对应的值
func (m *Map) Load(key any) (value any, ok bool) {
        // 第一步:原子读 read map(无锁!)
        read := m.read.Load()
        // 从 read 里找 key
        e, ok := read.m[key]
        // 如果 read 里没有,且 dirty 里有新数据(amended=true)
        if !ok && read.amended {
                // 加锁(慢路径)
                m.mu.Lock()
                // 双重检查(防止并发变化)
                read = m.read.Load()
                e, ok = read.m[key]
                // 还是没有,且 dirty 有新数据
                if !ok && read.amended {
                        // 从 dirty 里读
                        e, ok = m.dirty[key]
                        // miss 计数 +1,达到阈值就晋升 dirty → read
                        m.missLocked()
                }
                m.mu.Unlock()
        }
        // 没找到
        if !ok {
                return nil, false
        }
        // 原子获取值
        return e.load()
}

2. Store(写 / 更新)

func (m *Map) Store(key, value any) {
        // 第一步:尝试 read map 无锁更新
        read := m.read.Load()
        if e, ok := read.m[key]; ok && e.tryStore(&value) {
                return // 更新成功,直接返回
        }
        // 没更新成功 → 加锁,走 dirty
        m.mu.Lock()
        // 再次双重检查
        read = m.read.Load()
        if e, ok := read.m[key]; ok {
                // 存在 → 原子更新
                e.store(&value)
        } else if e, ok := m.dirty[key]; ok {
                // dirty 里存在 → 更新
                e.store(&value)
        } else {
                // 全新 key → 加入 dirty
                if m.dirty == nil {
                        // dirty 为空,先从 read 复制数据
                        m.dirtyDirty()
                }
                // 新key存入dirty
                m.dirty[key] = newEntry(value)
        }
        m.mu.Unlock()
}

3. missLocked(dirty 晋升 read —— 灵魂机制)

func (m *Map) missLocked() {
        m.misses++
        // 当 miss 次数 == dirty 元素数量时
        if m.misses < len(m.dirty) {
                return
        }
        // 直接把 dirty 变成新的 read!
        m.read.Store(&readOnly{m: m.dirty})
        // 清空 dirty
        m.dirty = nil
        m.misses = 0
}

五、sync.Map 完整流程图

1. 读(Load)流程图

image.png

2. 写(Store)流程图

image.png


六、总结

sync.Map 三大核心

  1. read map:无锁、只读、超快
  1. dirty map:加锁、存最新数据
  1. miss 晋升:read 命中太低 → dirty 直接变 read

优点

  • 读超级快(无锁)
  • 高并发安全
  • 读多写少场景性能碾压 map+mutex

缺点

  • 结构复杂
  • 写、删除都相对慢
  • 不适合频繁写入

七、面试(懂了说清楚就行)

1. sync.Map 为什么快?

读无锁、双 map 分离、miss 晋升机制,读多写少场景最优。

2. read 和 dirty 什么关系?

  • read 是快照
  • dirty 是最新数据
  • miss 够了 dirty 直接升级成 read

3. 为什么不直接用 map+mutex?

全局锁在高并发读会严重阻塞,sync.Map 读完全无锁。

4. entry 是什么?

存值的原子结构,保证原子读写,不产生竞态。

5. 什么场景用 sync.Map?

读多写少、全局缓存、并发配置、长生命周期 key。