sync.Map 底层实现

Category: 底层原理Updated: 2026-05-21Views: 15Public

sync.Map 的底层核心是空间换时间,通过两个 Map(read 和 dirty)的冗余结构,实现读写分离,最终达到针对特定场景的读操作无锁优化。它的 read 是一个只读的 map,提供无锁的并发读取,速度极快。写操作则会先操作一个加了锁的、可读写的 dirty map

dirty map 的数据积累到一定程度,或者 read map 中没有某个 key 时,sync.Map 会将 dirty map里的数据提升并覆盖掉旧的 read map,完成一次数据同步。

go 复制代码
type Map struct {
    mu Mutex
    read atomic.Pointer[readOnly] // 只读层,原子操作,无锁
    dirty map[any]*entry          // 脏数据层,受 mu 保护
    misses int                    // 统计 read 未命中次数
}

type readOnly struct {
    m       map[any]*entry
    amended bool // 如果 dirty 中包含 read 中没有的 key,则为 true
}

type entry struct {
    p unsafe.Pointer // 指向实际的数据,可能是 nil 或 expunged
}
  • read:主要负责高性能读取。由于它是原子指针,多个 Goroutine 并发读取不需要加锁。
  • dirty:负责写入新 Key。它是一个原生的 Go Map,所以操作它必须加 mu 锁。
  • entry:这是 readdirty 共享的。这意味着虽然有两张表,但同一个 Key 指向的是同一份数据的内存地址
go 复制代码
func (m *Map) Load(key any) (value any, ok bool) {
    read := m.loadReadOnly() // 原子读取
    e, ok := read.m[key]
    
    // Fast Path: 如果在 read 里找到了,直接返回
    if !ok && read.amended {
        m.mu.Lock() // 没找到且 dirty 有新数据,加锁
        // 双重检查
        read = m.loadReadOnly()
        e, ok = read.m[key]
        if !ok && read.amended {
            e, ok = m.dirty[key] // 从 dirty 里找
            m.missLocked()       // 记一次未命中次数
        }
        m.mu.Unlock()
    }
    if !ok {
        return nil, false
    }
    return e.load() // 返回 entry 指向的值
}

misses 计数器等于 len(dirty) 时,会触发 Promotion(提升) :将 dirty 赋给 read,并将 dirty 置为 nil。这保证了高频访问的 Key 最终都会沉淀到无锁的 read 层。

go 复制代码
func (m *Map) Store(key, value any) {
    // 如果 read 中已存在该 Key,尝试 atomic.CompareAndSwapPointer 原子更新
    read := m.loadReadOnly()
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        return
    }

    m.mu.Lock()
    read = m.loadReadOnly()
    if e, ok := read.m[key]; ok {
        if e.unexpungeLocked() {
            // 如果之前被标记为 expunged (彻底删除),需要同步到 dirty
            m.dirty[key] = e
        }
        e.storeLocked(&value) // 更新值
    } else if e, ok := m.dirty[key]; ok {
        e.storeLocked(&value) // Key 只在 dirty 中
    } else {
        // 这是一个全新的 Key
        if !read.amended {
            m.dirtyLocked() // 如果此时 dirty 为空,从 read 复制一份出来
            m.read.Store(&readOnly{m: read.m, amended: true})
        }
        m.dirty[key] = newEntry(value) // 写入 dirty
    }
    m.mu.Unlock()
}
  • dirtynil(刚发生过提升)并需要写入新 Key 时,dirtyLocked() 会遍历 read
  • 如果 read 中的某个值是 nil(逻辑删除),它会被标记为 expunged
  • 标记为 expunged 的 Key 不会被复制到新的 dirty 。这样可以清理掉陈旧的 Key,防止内存泄漏。
go 复制代码
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
    read := m.loadReadOnly()
    e, ok := read.m[key]
    if !ok && read.amended {
        m.mu.Lock()
        // ... 双重检查逻辑同 Load ...
        e, ok = m.dirty[key]
        delete(m.dirty, key) // 物理删除 dirty 里的 Key
        m.mu.Unlock()
    }
    if ok {
        return e.delete() // 逻辑删除:将 entry.p 设为 nil
    }
    return nil, false
}

为了性能,read 层的删除仅仅是将 entry.p 置为 nil(软删除)。真正的物理清理要等到下一次 dirtyLocked() 复制数据时,跳过那些 nil 的 Key。

  • p == nil: 逻辑删除,Key 还在 read 里。
  • p == expunged: 逻辑删除,且 Key 不在 dirty 里。
  • p == 正常地址: 数据存活。

为什么要设计 nil expunged 两种删除状态?

为了解决在 sync.Map 的读写分离架构下,高效、无锁地处理删除操作

因为 read map 本身是只读的,我们不能直接从中删除一个 key。所以,当用户调用 Delete 时,如果这个 key 只存在于 read map 中,系统并不会真的删除它,而是将它的值标记为一个特殊的已删除状态,即 expunged。后续的读操作如果看到 expunged 标记,就知道这个 key 其实已经不存在了,直接返回 nil, false。而 nil 则是一个中间状态,主要用于 dirty mapread map 的同步过程,表示这个 key 正在被删除或迁移。

简单来说,这两个状态就像是在只读的 read map 上打的逻辑删除补丁。它避免了因为一次 Delete 操作就引发加锁和 map 的整体复制,把真正的物理删除延迟到了dirty map 晋升为 read map 的那一刻,是典型的用状态标记来换取无锁性能的设计。

我们可以把 entry.p 的生命周期看作一个状态机:

  1. 正常状态:Key 存在,readdirty 可能都指向它。
  2. 软删除 (nil) :当你调用 Delete(key) 时,如果 Key 在 read 中,系统只是原子地把 p 设为 nil此时它还不是 expunged,因为它依然存在于 dirty 中(如果有 dirty 的话)。
  3. 彻底抹除 (expunged) :当 dirty 提升(被重置)并从 read 重新同步时,系统发现这个 Key 是 nil
    • 为了保持 dirty 的洁净,系统用 atomic.CompareAndSwapPointer (CAS) 把 pnil 改为 expunged
    • 关键点:被标记为 expunged 的 Key 不会被复制到新创建的 dirty map 中。

sync.Map 适用的场景?

sync.Map 适合读多写少的场景,而不是和写多读少的场景。

因为我们期望将更多的流量在 read map 这一层进行拦截,从而避免加锁访问 dirty map 对于更新,删除,读取,read map 可以尽量通过一些原子操作,让整个操作变得无锁化,这样就可以避免进一步加锁访问 dirty map。倘若写操作过多,sync.Map 基本等价于一把互斥锁 + map,其读写效率会大大下降。