Go sync.Map 源码深度解析:写时复制、无锁读与延迟删除的设计精髓

2026/3/28 笔记源码

# 背景

Go 原生 map 不支持并发读写,多 goroutine 直接并发操作会触发 panic。常规解决方案是搭配 sync.Mutex/sync.RWMutex 加锁,但这在读多写少、键值对更新远多于新增的场景下,锁竞争会成为严重的性能瓶颈。

sync.Map 正是为解决这个场景而生,它通过读写分离、写时复制(COW)、延迟删除、原子操作替代锁等一系列精妙设计,实现了绝大多数读操作的无锁化,大幅降低了高并发场景下的锁竞争开销。

本文基于 Go 1.25.0 版本的 sync/map.go (opens new window) 源码,深入拆解其无锁读、写时复制、延迟删除三大核心设计的精髓,帮助开发者吃透高并发容器的底层设计思想。


# 一、核心数据结构:双map+entry状态机的底层基石

sync.Map 的所有精妙设计,都建立在极简又极具巧思的数据结构之上,核心是read/dirty 双map读写分离,以及entry 三状态的原子操作

# 核心源码定义

// Map 是并发安全的map,零值可用,首次使用后不可复制
type Map struct {
    // 互斥锁,保护dirty map的所有操作,以及read map的结构变更
    mu Mutex

    // read 存储只读的readOnly结构体,通过原子操作存取,读操作完全无锁
    // 其中的map本身不会被修改结构(新增/删除key),仅会修改entry内的value指针
    read atomic.Pointer[readOnly] // 内部存储readOnly类型

    // dirty 是写操作的核心map,包含全量有效key(read未删除的key + 新增key)
    // 所有操作必须加mu锁,当未命中次数达到阈值时,会被提升为新的read map
  	dirty map[any]*entry

    // misses 统计read map未命中、需要加锁访问dirty的次数
    // 达到len(dirty)阈值时,触发dirty到read的提升
    misses int
}

// readOnly 是原子存储在Map.read中的不可变结构体
type readOnly struct {
    m       map[any]*entry
    amended bool // 标记dirty中存在read.m里没有的新key
}

// expunged 哨兵指针,标记entry已被彻底清除,不存在于dirty map中
var expunged = new(any)

// entry 是value的包装结构体,核心是支持原子操作的unsafe.Pointer指针
// 实现了value的无锁更新,避免修改map本身的结构
type entry struct {
    p atomic.Pointer[any] // 指向*interface{}类型的value
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 核心设计解读

  1. 双map读写隔离

    • read map:读热点区,所有读操作优先无锁访问,仅支持原子修改entry内的value,不会新增/删除key、修改map结构,彻底避免并发读写panic。

    • dirty map:写操作区,所有map结构变更(新增key、清理删除key)都在这里完成,操作必须加锁,是冷数据区。

  2. amended 快速判断标记:仅用一个bool值,就能让读操作快速判断dirty中是否有read没有的新key,避免无意义的加锁操作。

  3. entry 原子包装层:原生map的value不可寻址,无法直接原子修改。通过entry包装成指针后,map仅存储entry的指针,修改value时只需原子更新entry内的指针,无需修改map结构,为无锁更新、无锁删除奠定了基础。

  4. entry 三状态机:是删除逻辑的核心,三个状态通过原子指针区分:

    • 有效状态p 指向有效的*interface{},key存在,可正常读取value;

    • 软删除状态p = nil,key被标记删除,仍存在于read map中,读取时返回不存在;

    • 彻底清除状态p = expunged,key被标记删除,且已从dirty map中剔除,不会被复制到新的dirty中。


# 二、无锁读的实现:99%场景的零开销访问

sync.Map 最核心的性能优势,就是绝大多数读操作完全无锁,无需竞争互斥锁,这也是它在读多写少场景下远超 mutex+map 的核心原因。

# Load 方法核心源码

// Load 读取key对应的value,ok标记key是否存在
func (m *Map) Load(key any) (value any, ok bool) {
    // 1. 原子加载readOnly,完全无锁,无竞争
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]

    // 2. 仅当read中无key,且dirty有新key时,才需要加锁
    if !ok && read.amended {
        m.mu.Lock()
        // 3. 双重检查:加锁后再次加载read,避免并发过程中dirty已被提升
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]

        // 4. 再次确认read无key,且dirty有新key,才访问dirty
        if !ok && read.amended {
            e, ok = m.dirty[key]
            // 5. 未命中计数+1,达到阈值时触发dirty提升
            m.missLocked()
        }
        m.mu.Unlock()
    }

    // 6. 无对应entry,返回不存在
    if !ok {
        return nil, false
    }
    // 7. 原子加载entry中的value
    return e.Load()
}

// missLocked 处理未命中计数,达到阈值时将dirty提升为新的read
func (m *Map) missLocked() {
    m.misses++
    // 阈值为dirty的长度,平衡提升频率与访问开销
    if m.misses < len(m.dirty) {
        return
    }
    // 原子替换read为dirty,零拷贝!仅传递map引用,无数据复制
    m.read.Store(readOnly{m: m.dirty})
    // 清空dirty,重置计数
    m.dirty = nil
    m.misses = 0
}

// Load 原子加载entry中的value
func (e *entry) Load() (value any, ok bool) {
    p := atomic.LoadPointer(&e.p)
    // 指针为nil或expunged,说明key已被删除
    if p == nil || p == expunged {
        return nil, false
    }
    return *(*any)(p), true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 无锁设计的精髓

  1. 读操作优先无锁访问:所有读操作第一步都是原子加载readOnly,完全无锁。对于读多写少、key基本不变的场景,几乎所有读操作都在这一步完成,零锁开销,无上下文切换。

  2. 双重检查(Double Check)范式:加锁后会再次加载read map,避免并发过程中其他goroutine已将dirty提升为read,导致重复访问dirty;同时保证了并发安全,彻底避免竞态问题,是无锁编程的经典实践。

  3. 自适应的未命中阈值设计misses 的阈值设为 len(dirty),这个设计极具巧思:

    • 当dirty很小时,阈值很低,快速触发提升,避免频繁加锁访问小dirty;

    • 当dirty很大时,阈值很高,避免频繁触发全量map替换,减少性能抖动;

    • 保证只有当访问dirty的累计开销,超过提升dirty的一次性开销时,才会触发提升,完美平衡了读写性能。

  4. entry的原子读保障:即使在read map中命中key,读取value也是通过原子操作加载entry的指针,保证并发更新value时,读操作不会拿到脏数据,同时全程无需加锁。


# 三、写时复制(COW):平衡读写性能的核心设计

写时复制(Copy-On-Write)是sync.Map的核心设计思想,它彻底避免了传统加锁map中读写竞争锁的问题,把map结构的变更和读操作完全隔离开,仅在必要时执行浅拷贝,把复制开销降到了最低。

sync.Map的写操作分为更新已存在的key新增不存在的key两种场景,分别对应了无锁更新和写时复制的核心逻辑。

# Store 方法核心源码

// Store 存储key-value对
func (m *Map) Store(key, value any) {
    // 1. 先无锁加载read,尝试直接更新已存在的key
    read, _ := m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        // 已存在的key,原子更新成功,直接返回,完全无锁!
        return
    }

    // 2. 无法无锁更新,加锁处理
    m.mu.Lock()
    // 双重检查,加锁后再次确认read中的状态
    read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
        // 2.1 key在read中存在,但被标记为expunged,需先恢复到dirty中
        if e.unexpungeLocked() {
            // 恢复成功,将entry加入dirty,保证dirty包含全量有效key
            m.dirty[key] = e
        }
        // 原子更新entry的value
        e.storeLocked(&value)
    } else if e, ok := m.dirty[key]; ok {
        // 2.2 key不在read中,但在dirty中,直接更新dirty内的entry
        e.storeLocked(&value)
    } else {
        // 2.3 全新的key,read和dirty中均不存在,触发写时复制逻辑
        if !read.amended {
            // amended为false,说明dirty当前为nil,需初始化dirty
            // 写时复制核心:将read中未删除的key浅拷贝到dirty
            m.dirtyLocked()
            // 标记dirty包含read没有的新key
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        // 新key仅加入dirty,read map完全不修改
        m.dirty[key] = newEntry(value)
    }
    m.mu.Unlock()
}

// tryStore 尝试无锁原子更新entry的value,仅当entry非expunged状态时成功
func (e *entry) tryStore(i *any) bool {
    for {
        p := atomic.LoadPointer(&e.p)
        // entry已被彻底清除,无法无锁更新,返回失败
        if p == expunged {
            return false
        }
        // CAS原子更新value指针
        if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
            return true
        }
    }
}

// unexpungeLocked 将expunged状态的entry恢复为nil,确保被加入dirty
// 必须持有mu锁时调用
func (e *entry) unexpungeLocked() (wasExpunged bool) {
    return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}

// storeLocked 强制原子更新entry的value,必须持有mu锁时调用
func (e *entry) storeLocked(i *any) {
    atomic.StorePointer(&e.p, unsafe.Pointer(i))
}

// dirtyLocked 初始化dirty,将read中未删除的entry浅拷贝到dirty
// 必须持有mu锁时调用
func (m *Map) dirtyLocked() {
    if m.dirty != nil {
        return
    }

    read, _ := m.read.Load().(readOnly)
    // 初始化dirty,容量与read map一致,避免多次扩容
    m.dirty = make(map[any]*entry, len(read.m))
    // 遍历read map,浅拷贝未删除的entry
    for k, e := range read.m {
        // 尝试标记entry为expunged,仅未删除的entry会被复制到dirty
        if !e.tryExpungeLocked() {
            m.dirty[k] = e
        }
    }
}

// tryExpungeLocked 尝试将nil状态的entry标记为expunged
// 返回true表示entry已被删除,无需复制到dirty;必须持有mu锁时调用
func (e *entry) tryExpungeLocked() (isExpunged bool) {
    p := atomic.LoadPointer(&e.p)
    for p == nil {
        // 将软删除的entry,CAS设置为expunged,标记为彻底清除
        if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
            return true
        }
        p = atomic.LoadPointer(&e.p)
    }
    return p == expunged
}

// newEntry 创建一个新的entry
func newEntry(i any) *entry {
    return &entry{p: unsafe.Pointer(&i)}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

# 写时复制的设计精髓

# 场景1:更新已存在的key——完全无锁的原子更新

这是sync.Map最极致的性能优化点:对于已存在的key,更新value全程无需加锁

  • 当Store一个已存在的key时,首先在read map中找到对应的entry,调用tryStore方法,通过CAS原子操作更新entry内的p指针。

  • 整个过程完全没有修改map的结构(没有新增/删除key,map哈希表完全不变),仅修改了entry内的指针,因此无需加锁,也不会和读操作产生任何竞争。

  • 这也是为什么sync.Map更新远多于新增的场景下,性能远超mutex+map——传统方案更新也要加锁,而sync.Map实现了零锁开销的更新。

# 场景2:新增key——写时复制的懒初始化与浅拷贝

新增key是写时复制的核心场景,这里的设计彻底避免了修改read map的结构,把所有结构变更都限制在加锁的dirty map中,同时通过懒拷贝把复制开销降到最低。

  1. 新增key绝不修改read map:read map被多个goroutine无锁并发读,直接修改其结构会触发并发map读写panic。因此所有新增key只会写入dirty map,read map完全不动,彻底隔离了读写操作的竞争。

  2. 懒初始化与写时复制:第一次新增key时,dirty为nil,此时会调用dirtyLocked方法,把read map中未删除的entry浅拷贝到dirty中。

    • 这里的复制是浅拷贝:仅复制map的key和entry的指针,不会复制value本身,即使read map体量很大,复制开销也极低。

    • 这就是写时复制的核心:只有当需要新增key时,才会复制read的内容到dirty,平时完全不执行复制操作,把复制开销延迟到第一次写的时候,避免了不必要的性能损耗。

  3. amended标记的联动:初始化dirty后,会将readOnly的amended标记设为true,告知后续的读操作:dirty中存在read没有的新key,读不到时需要加锁访问dirty。

  4. 零拷贝的dirty提升:当misses达到阈值后,会直接将dirty原子存储到read中,这个过程是零拷贝的——仅把dirty的map引用赋值给readOnly的m字段,没有任何数据复制,原来的dirty直接变成新的read map,随后dirty被置为nil,等待下一次新增key时再初始化。

    • 写时复制机制保证了:若旧的read map还有goroutine在读,依然可以正常访问,因为旧的map是不可变的,不会被修改,直到无引用后被GC回收,完全无并发安全问题。

误区纠正:很多人以为写时复制是每次写都要全量复制map,实则sync.Map的COW设计,仅在第一次新增key时浅拷贝read内容到dirty,后续新增key直接加锁写入dirty,无需复制;仅当dirty提升为read后,下一次新增key才会再次复制,复制频率由misses阈值严格控制,开销极低。


# 四、延迟删除的优雅设计:避免锁竞争与map结构频繁变更

sync.Map的删除操作,是最容易被忽略却又极其精妙的设计。它没有采用直接删除key的方式,而是用软删除+延迟清理的机制,把删除操作也变成了无锁的原子操作,仅在必要时才真正清理删除的key,大幅减少了锁的持有时间和map结构的变更频率。

# Delete 方法核心源码

// Delete 删除key
func (m *Map) Delete(key any) {
    m.LoadAndDelete(key)
}

// LoadAndDelete 删除key,返回被删除的value和是否存在
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
    // 1. 先无锁加载read,查找key
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]

    // 2. read中无key,且dirty有新key,加锁处理
    if !ok && read.amended {
        m.mu.Lock()
        // 双重检查
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            // key仅在dirty中,直接从dirty删除,这是真正的结构删除
            e, ok = m.dirty[key]
            delete(m.dirty, key)
            // 未命中计数+1
            m.missLocked()
        }
        m.mu.Unlock()
    }

    // 3. 无对应key,直接返回
    if !ok {
        return nil, false
    }
    // 4. 找到entry,原子标记为软删除,完全无锁!
    return e.delete()
}

// delete 原子将entry标记为删除,返回旧的value和是否存在
func (e *entry) delete() (value any, ok bool) {
    for {
        p := atomic.LoadPointer(&e.p)
        // 已被删除,直接返回
        if p == nil || p == expunged {
            return nil, false
        }
        // CAS将p设为nil,标记为软删除
        if atomic.CompareAndSwapPointer(&e.p, p, nil) {
            return *(*any)(p), true
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 延迟删除的设计精髓

# 1. 软删除:无锁的删除操作

对于存在于read map中的key,删除操作全程无需加锁!

  • 不是直接从read map中删除key(那样会修改map结构,需要加锁,还会触发并发读写panic),而是通过CAS原子操作,把entry的p指针设为nil,标记为软删除。

  • 软删除后的key仍存在于read map中,但读取时会判断p=nil,返回不存在,完全不影响读操作的正确性,同时整个删除过程无锁、无竞争。

# 2. 延迟清理:开销平摊,无额外性能损耗

软删除的key,会在下一次初始化dirty的时候被真正清理,无需单独的清理逻辑。

  • 当新增key需要初始化dirty时,会调用dirtyLocked方法遍历read map的所有entry,调用tryExpungeLocked方法。

  • 对于已软删除(p=nil)的entry,会通过CAS将p设为expunged,标记为彻底清除,不会被复制到dirty中

  • 当dirty被提升为新的read map后,旧的read map失去引用,会被GC回收,这些被标记为expunged的key就会被彻底清理,完全不需要额外的锁操作和遍历开销。

  • 这个设计的精妙之处在于:把删除key的清理开销,合并到了新增key的写时复制过程中,不需要单独启动goroutine清理,也不会在删除时占用锁的时间,把批量清理的开销平摊到了极少的写操作中。

# 3. expunged状态:解决软删除的并发冲突

expunged哨兵指针是整个删除逻辑的灵魂,它完美解决了软删除后,再次Store同一个key的并发冲突问题。

  • 当一个entry被标记为expunged后,说明它已被从dirty中剔除,read map中有这个key,但dirty中没有。

  • 此时如果再次Store这个key,不能直接无锁更新entry,因为dirty中没有这个key,会导致dirty提升时,这个key丢失。

  • 因此tryStore方法会判断,若entry是expunged状态,直接返回失败,进入加锁逻辑:调用unexpungeLocked方法将entry从expunged状态恢复为nil,再把entry加入dirty,最后更新value,保证dirty始终包含全量有效key。

  • expunged状态完美区分了“软删除但未清理的entry”和“已清理出dirty的entry”,解决了软删除后再次写入的并发安全问题,同时没有增加任何额外的锁开销。


# 五、设计精髓总结与开发启示

sync.Map 不是为了替代所有的mutex+map方案,而是针对读多写少、键值对更新频繁、新增删除少的场景做了极致优化,它的核心设计思想,值得所有开发者学习和借鉴:

  1. 读写分离,冷热隔离:把读热点数据放到无锁的read map,把写操作(结构变更)放到加锁的dirty map,让绝大多数读操作完全避开锁竞争,这是高并发容器设计的核心思路。

  2. 写时复制,懒加载+浅拷贝:不是每次写都复制,而是把复制延迟到必要的时候,且仅做浅拷贝,把复制开销降到最低;同时通过原子替换实现零拷贝的map升级,完美平衡了读写性能。

  3. 原子操作替代锁:把value包装成支持原子操作的entry,让更新、删除操作都可以通过CAS原子操作完成,完全不需要锁,大幅降低了并发竞争的开销。

  4. 延迟处理,平摊开销:删除操作采用软删除+延迟清理,把批量清理的开销平摊到极少的写操作中,避免了高频操作的性能损耗,同时减少了锁的持有时间。

  5. 双重检查,保证并发安全:所有加锁操作前,都先做无锁检查,加锁后再次做双重检查,既避免了无意义的加锁,又保证了并发安全,是无锁编程的经典范式。

# 适用场景与避坑指南

  • 适合场景:读多写少,尤其是key的更新远多于新增;多个goroutine读写不相交的key集合;并发访问的热点key集中,冷key很少。

  • 不适合场景:写多读少,尤其是频繁新增大量新key的场景(会导致dirty频繁初始化和提升,性能反而不如mutex+map);遍历操作非常频繁的场景。