Map is like a Go map[interface{}]interface{} but is safe for concurrent use
by multiple goroutines without additional locking or coordination.
Loads, stores, and deletes run in amortized constant time.
上面一段是官方对sync.Map
的描述,从描述中看,sync.Map
跟map
很像,sync.Map
的底层实现也是依靠了map
,但是sync.Map
相对于 map
来说,是并发安全的。
结构概览
sync.Map
sync.Map的结构体了
1 | type Map struct { |
readOnly
sync.Map.read属性所对应的结构体了,这里不太明白为什么不把readOnly结构体的属性直接放入到sync.Map结构体里
1 | type readOnly struct { |
entry
entry就是unsafe.Pointer,记录的是数据存储的真实地址
1 | type entry struct { |
结构示意图
通过上面的结构体,我们可以简单画出来一个结构示意图
流程分析
我们通过下面的动图(也可以手动debug),看一下在我们执行Store
Load
Delete
的时候,这个结构体的变换是如何的,先增加一点我们的认知
1 | func main() { |
以上面代码为例,我们看一下m的结构变换
源码分析
新增key
新增一个key value,通过Store
方法来实现
1 | func (m *Map) Store(key, value interface{}) { |
tryLock
1 | func (e *entry) tryStore(i *interface{}) bool { |
tryLock会对key对应的值,进行判断,是否被设置为了expunged,这种情况下不能直接更新
dirtyLock
这里就是设置 expunged 标志的地方了,而这个函数正是将read中的数据同步到dirty的操作
1 | func (m *Map) dirtyLocked() { |
tryExpungeLocked
通过原子操作,给entry,key对应的值设置 expunged 标志
1 | func (e *entry) tryExpungeLocked() (isExpunged bool) { |
unexpungeLocked
1 | func (e *entry) unexpungeLocked() (wasExpunged bool) { |
根据上面分析,我们发现,在新增的时候,分为四种情况:
- key原先就存在于read中,获取key所对应内存地址,原子性修改
- key存在,但是key所对应的值被标记为 expunged,解锁,解除标记,并更新dirty中的key,与read中进行同步,然后修改key对应的值
- read中没有key,但是dirty中存在这个key,直接修改dirty中key的值
- read和dirty中都没有值,先判断自从read上次同步dirty的内容后有没有再修改过dirty的内容,没有的话,先同步read和dirty的值,然后添加新的key value到dirty上面
当出现第四种情况的时候,很容易产生一个困惑:既然read.amended == false,表示数据没有修改,为什么还要将read的数据同步到dirty里面呢?
这个答案在Load
函数里面会有答案,因为,read同步dirty的数据的时候,是直接把dirty指向map的指针交给了read.m,然后将dirty的指针设置为nil,所以,同步之后,dirty就为nil
下面看看具体的实现
读取(Load)
1 | func (m *Map) Load(key interface{}) (value interface{}, ok bool) { |
为什么找到了p,但是p对应的值为nil呢?这个答案在后面解析Delete
函数的时候会被揭晓
missLocked
1 | func (m *Map) missLocked() { |
删除(Delete)
这里的删除并不是简单的将key从map中删除
1 | func (m *Map) Delete(key interface{}) { |
根据上面的逻辑可以看出,删除的时候,存在以下几种情况
- read中没有,且Map存在修改,则尝试删除dirty中的map中的key
- read中没有,且Map不存在修改,那就是没有这个key,无需操作
- read中有,尝试将key对应的值设置为nil,后面读取的时候就知道被删了,因为dirty中map的值跟read的map中的值指向的都是同一个地址空间,所以,修改了read也就是修改了dirty
遍历(Range)
遍历的逻辑就比较简单了,Map只有两种状态,被修改过和没有修改过
修改过:将dirty的指针交给read,read就是最新的数据了,然后遍历read的map
没有修改过:遍历read的map就好了
1 | func (m *Map) Range(f func(key, value interface{}) bool) { |
适用场景
在官方介绍的时候,也对适用场景做了说明
The Map type is optimized for two common use cases:
(1) when the entry for a given key is only ever written once but read many times, as in caches that only grow,
(2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys.
In these two cases, use of a Map may significantly reduce lock contention compared to a Go map paired with a separate Mutex or RWMutex.
通过对源码的分析来理解一下产生这两条规则的原因:
读多写少:读多写少的环境下,都是从read的map去读取,不需要加锁,而写多读少的情况下,需要加锁,其次,存在将read数据同步到dirty的操作的可能性,大量的拷贝操作会大大的降低性能
读写不同的key:sync.Map是针对key的值的原子操作,相当于加锁加载 key上,所以,多个key的读写是可以同时并发的