在大多数语言中原始map都不是一个线程安全的数据结构,那若是要在多个线程或者goroutine中对线程进行更改就须要加锁,除了加1个大锁,不一样的语言还有不一样的优化方式, 像在java和go这种语言其实都采用的是链表法来进行map的实现,本文也主要分析这种场景java
在go语言中实现多个goroutine并发安全访问修改的map的方式,主要有以下三种:安全
实现方式 | 原理 | 适用场景 |
---|---|---|
map+Mutex | 经过Mutex互斥锁来实现多个goroutine对map的串行化访问 | 读写都须要经过Mutex加锁和释放锁,适用于读写比接近的场景 |
map+RWMutex | 经过RWMutex来实现对map的读写进行读写锁分离加锁,从而实现读的并发性能提升 | 同Mutex相比适用于读多写少的场景 |
sync.Map | 底层通分离读写map和原子指令来实现读的近似无锁,并经过延迟更新的方式来保证读的无锁化 | 读多修改少,元素增长删除频率不高的状况,在大多数状况下替代上述两种实现 |
上面三种实现具体的性能差别可能还要针对不一样的具体的业务场景和平台、数据量等所以来进行综合的测试,源码的学习更多的是了解其实现细节,以便在出现性能瓶颈的时候能够进行分析,找出解决解决方案微信
在Mutex和RWMutex实现的并发安全的map中map随着时间和元素数量的增长、删除,容量会不断的递增,在某些状况下好比在某个时间点频繁的进行大量数据的增长,而后又大量的删除,其map的容量并不会随着元素的删除而缩小,而在sync.Map中,当进行元素从dirty进行提高到read map的时候会进行重建,可能会缩容数据结构
并发访问map读的主要问题实际上是在扩容的时候,可能会致使元素被hash到其余的地址,那若是个人读的map不会进行扩容操做,就能够进行并发安全的访问了,而sync.map里面正是采用了这种方式,对增长元素经过dirty来进行保存并发
经过read只读和dirty写map将操做分离,其实就只须要经过原子指令对read map来进行读操做而不须要加锁了,从而提升读的性能ide
上面提到增长元素操做可能会先增长大dirty写map中,那针对多个goroutine同时写,其实就须要进行Mutex加锁了源码分析
上面提到了read只读map和dirty写map, 那就会有个问题,默认增长元素都放在dirty中,那后续访问新的元素若是都经过 mutex加锁,那read只读map就失去意义,sync.Map中采用一直延迟提高的策略,进行批量将当前map中的全部元素都提高到read只读map中从而为后续的读访问提供无锁支持性能
map里面存储数据都会涉及到一个问题就是存储值仍是指针,存储值可让 map做为一个大的的对象,减轻垃圾回收的压力(避免扫描全部小对象),而存储指针能够减小内存利用,而sync.Map中其实采用了指针结合惰性删除的方式,来进行 map的value的存储学习
惰性删除是并发设计中一中常见的设计,好比删除某个个链表元素,若是要删除则须要修改先后元素的指针,而采用惰性删除,则一般只须要给某个标志位设定为删除,而后在后续修改中再进行操做,sync.Map中也采用这种方式,经过给指针指向某个标识删除的指针,从而实现惰性删除测试
type Map struct {
mu Mutex
// read是一个readOnly的指针,里面包含了一个map结构,就是咱们说的只读map对该map的元素的访问
// 不须要加锁,只须要经过atomic加载最新的指针便可
read atomic.Value // readOnly
// dirty包含部分map的键值对,若是要访问须要进行mutex获取
// 最终dirty中的元素会被所有提高到read里面的map中
dirty map[interface{}]*entry
// misses是一个计数器用于记录从read中没有加载到数据
// 尝试从dirty中进行获取的次数,从而决定将数据从dirty迁移到read的时机
misses int
}复制代码
只读map,对该map元素的访问不须要加锁,可是该map也不会进行元素的增长,元素会被先添加到dirty中而后后续再转移到read只读map中,经过atomic原子操做不须要进行锁操做
type readOnly struct {
// m包含全部只读数据,不会进行任何的数据增长和删除操做
// 可是能够修改entry的指针由于这个不会致使map的元素移动
m map[interface{}]*entry
// 标志位,若是为true则代表当前read只读map的数据不完整,dirty map中包含部分数据
amended bool
}
复制代码
entry是sync.Map中值得指针,若是当p指针指向expunged这个指针的时候,则代表该元素被删除,但不会当即从map中删除,若是在未删除以前又从新赋值则会重用该元素
type entry struct {
// 指向元素实际值得指针
p unsafe.Pointer // *interface{}
}复制代码
元素若是存储在只读map中,则只须要获取entry元素,而后修改其p的指针指向新的元素就能够了,由于是原地操做因此map不会发生变化
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}复制代码
若是此时发现元素存在只读 map中,则证实以前有操做触发了从dirty到read map的迁移,若是此时发现存在则修改指针便可
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() {
// The entry was previously expunged, which implies that there is a
// non-nil dirty map and this entry is not in it.
// 若是key以前已经被删除,则这个地方会将key从进行nil覆盖以前已经删除的指针
// 而后将它加入到dirty中
m.dirty[key] = e
}
// 调用atomic进行value存储
e.storeLocked(&value)
}复制代码
若是元素存在dirty中其实同read map逻辑同样,只须要修改对应元素的指针便可
} else if e, ok := m.dirty[key]; ok {
// 若是已经在dirty中就会直接存储
e.storeLocked(&value)
} else {复制代码
若是元素以前不存在当前Map中则须要先将其存储在dirty map中,同时将amended标识为true,即当前read中的数据不全,有一部分数据存储在dirty中
// 若是当前不是在修正状态
if !read.amended {
// 新加入的key会先被添加到dirty map中, 并进行read标记为不完整
// 若是dirty为空则将read中的全部没有被删除的数据都迁移到dirty中
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)复制代码
在刚初始化和将全部元素迁移到read中后,dirty默认都是nil元素,而此时若是有新的元素增长,则须要先将read map中的全部未删除数据先迁移到dirty中
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
read, _ := m.read.Load().(readOnly)
m.dirty = make(map[interface{}]*entry, len(read.m))
for k, e := range read.m {
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}复制代码
当持续的从read访问穿透到dirty中后,就会触发一次从dirty到read的迁移,这也意味着若是咱们的元素读写比差比较小,其实就会致使频繁的迁移操做,性能其实可能并不如rwmutex等实现
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
m.read.Store(readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}复制代码
Load数据的时候回先从read中获取,若是此时发现元素,则直接返回便可
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]复制代码
加锁后会尝试从read和dirty中读取,同时进行misses计数器的递增,若是知足迁移条件则会进行数据迁移
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// 这里将采起缓慢迁移的策略
// 只有当misses计数==len(m.dirty)的时候,才会将dirty里面的数据所有晋升到read中
m.missLocked()
}复制代码
数据删除则分为两个过程,若是数据在read中,则就直接修改entry的标志位指向删除的指针便可,若是当前read中数据不全,则须要进行dirty里面的元素删除尝试,若是存在就直接从dirty中删除便可
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
delete(m.dirty, key)
}
m.mu.Unlock()
}
if ok {
e.delete()
}
}复制代码
由于mutex互斥的是全部操做,包括dirty map的修改、数据的迁移、删除,若是在进行m.lock的时候,已经有一个提高dirty到read操做在进行,则执行完成后dirty其实是没有数据的,因此此时要再次进行read的重复读
微信号:baxiaoshi2020
关注公告号阅读更多源码分析文章
更多文章关注 www.sreguide.com
本文由博客一文多发平台 OpenWrite 发布!