目录
一. sync.Map 基础使用示例
- sync.Map 的使用方法与普通的 map 类似,可以通过类似于读写锁的方式进行读写操作,提供了以下几个方法
- Store(key, value):向哈希表中存储一个键值对。
- Load(key):从哈希表中获取指定键的值。
- LoadOrStore(key, value):从哈希表中尝试获取指定键的值。如果该键不存在,则存储给定的键值对并返回 value。如果键已经存在,则直接返回当前值。
- Delete(key):从哈希表中删除指定键及其对应的值。
- Range():遍历哈希表中的所有键值对,并调用指定的处理函数进行处理。
- sync.Map通过读写分离的机制,在读取时不需要加锁,在写入时则会进行细粒度的锁定,以保证数据的一致性和并发安全性
- 注意: 使用 sync.Map 时不需要调用 make() 来初始化。因为 sync.Map 的 zero value 就是一个可用的空 map,可以直接对其进行 Store、Load、Delete 和 Range 等操作
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// 存储键值对
m.Store("foo", 1)
m.Store("bar", "hello")
// 根据键获取值
if v, ok := m.Load("foo"); ok {
fmt.Println(v)
}
// 删除键值对
m.Delete("bar")
// 遍历所有的键值对
m.Range(func(key, value interface{}) bool {
fmt.Printf("key: %v, value: %v\n", key, value)
return true
})
}
二. 底层
- 不同版本的底层实现是不同的
- 查看sync.Map底层结构
type Map struct {
mu Mutex
// 把read看成一个安全的只读快照表,实际对应的是readOnly,
read atomic.Value // readOnly
// dirty需要使用上面的mu加锁才能访问里面的元素,
//dirty中包含所有在read字段中但未被expunged(删除)的元素,
//重点包含最新的 KV 对,等时机成熟,dirty 会被转换为 read, 然后该字段会被置为空
dirty map[interface{}]*entry
// misses是一个计数器,记录在从read中读取数据的时候,没有命中的次数,
//每次从 read 中没找到回到 dirty 中查询都会导致 misses 自增一,
//当misses > len(dirty) 时,就会触发dirty转换
misses int
}
- Map中的read字段是atomic.Value类型,它里面实际存储的结构为readOnly,readOnly以原子方式存储在read中
// readOnly是存在Map结构中read字段中的内容,它以原子方式存储
type readOnly struct {
m map[interface{}]*entry
// amended为true表示dirty中包含read中没有的数据,
//为false表示dirty中的数据在read都存在
amended bool // true if the dirty map contains some key not in m.
}
// dirty 代表状态不稳定的哈希表,即正在执行写操作的哈希表
type dirty struct {
m map[interface{}]*entry
dirty map[interface{}]*entry // 暂存 dirty 表中需要删除的元素
misses int // 记录 readOnly 中查找 fail + dirty 中查找 success 的次数
}
- 无论是dirty还是read中的m它们都是内建的map,在存储value时会将value封装为entry类型。entry是对interface{}做了一个结构体的包装。
type entry struct {
p unsafe.Pointer // *interface{}
}
- entry中的p属性实际有三种状态分别为: nil, expunged删除,正常,
- 在sync.Map删除一个key时,并不是立即删除,而是将key对应的value标记为nil或者expunged,在以后的处理过程中才有机会真正删除
var expunged = unsafe.Pointer(new(interface{}))
先简述一下sync.Map如何实现线程安全的
- 在java中ConcurrentHashMap1.7版本是通过分段锁,1.8版本是通过锁桶的首节点+synchronized实现的线程安全,通过降低锁粒度来提升性能,sync.Map 的思路是尽量通过读写分离与原子操作实现线程安全
- 在sync.Map中提供了:
- read属性是一个atomic.Value原子类型,内部存储的实际是一个map
- dirty属性: 需要加锁访问,包含所有在read字段,重点是存储了最新的 KV 对,一个新的键值对会被存储在这,等时机成熟,dirty 会被转换为 read, 然后该字段会被置为空,由于 dirty 中的数据总是比 read 中的更新,所以在查询修改等操作中,read 中如果找不到还需要回到 dirty 中找
- mu: 用于对 dirty 操作时保障并发安全的锁
- misses 计数器属性用来控制什么时候将dirty 转换为 read
- 在存储KV键值对时,数据首先保存到dirty中,dirty需要加锁访问,所以在查询修改等时,如果read 中不存在需要通过 dirty查找,通过misses 计数器记录通过read读取数据没有命中的次数,当misses与dirty长度相同时会把dirty转换为 read, 然后将dirty置为空,read 与 dirty 中存储的 Value 都是 entry 的指针,同一个key指向的是同一个value
- 为了提高性能,针对保存数据的entry提供了expunged 删除状态:
- 首先删除数据时并不是直接删除,会先打expunged 标记
- 为了提高性能,假设添加的key在read中已经存在但是对应的entry.p不是expunged状态,此时不加锁更新即可,但是当key对应的entry.p=expunged状态,需要在read和dirty中都添加,这个过程是需要进行加锁的,expunged状态,是为了dirty为空的时候,直接对read进行操作不用加锁,提升程序性能
Store 插入
- Store()添加数据时,有新增或更新两种情况,同时也有加锁与不加锁两种情况
- 不加锁情况:
- key在read中,p=nil(p是entry中的),并且dirty中不存在数据,
- key在read中,p=&entry,表示key存在,且指向一个真实的value
- 加锁情况:
- key在read中,p=expunged表示key已被删除,dirty中存在数据且该key不在dirty中,需要加锁处理
- key不在read中 使用到了锁的时候,性能就会下降
- 所以sync.Map比较适合那些只会增长的缓存系统,可以进行更新操作,但最好不要删除,并且不要频繁地增加新元素,因为新增元素,会加入到dirty中,对dirty操作需要加锁。在dirty为nil,新加元素的时候,会创建一个新dirty,将read中的有效的key-value键值对复制到新dirty中,read中已删除的key(value为nil或者expunged)不会复制到dirty中
- sync.Map基于类似读写分离的机制,在读取时不需要加锁,在写入时则会进行细粒度的锁定,以保证数据的一致性和并发安全性,查看Store()源码:
- 首先,在readOnly只读快照中查找键是否存在,如果存在调用entry上的tryStore()方法修改值
- 如果readOnly只读快照中不存在,则通过Map中提供的mu写入锁属性调用Lock()函数加锁
- 加锁后首先执行"m.read.Load().(readOnly)", 通过readOnly再次判断是否存在,并且检查是否有其他协程在并发操作
- 如果readOnly中存在,调用entry上的storeLocked()方法修改该key的value,并且会判断当前key如果是expunged状态(表示该键所对应的值已经被删除,只有在 dirty 部分中才会出现该状态),从 dirty 中移除,并加入readOnly中
- 如果没有在readOnly中,会判断是否在dirty 中,存在则修改其 value
- 如果 key 既不在 readOnly 中,也不在 dirty 中,则调用newEntry()函数,将value封装为entry结构体变量插入到 dirty 中,如果dirty为空,还需要进行初始化,并将 readOnly 部分的 amended 标记设置为 true
- 最后释放锁,并且通过 CAS 原子操作更新 readOnly 部分,将 dirty 部分更新为最新的 readOnly
- 注意在存储value时,会将value封装为一个entry结构体,比如如果key不存在会调用newEntry()将value封装为entry结构体,如果key存在会调用entry上的storeLocked()修改
// Store保存或更新一个键值对
func (m *Map) Store(key, value interface{}) {
// 检查key是否在read中存在
read, _ := m.read.Load().(readOnly)
// 如果key在read中,有3种情况:
// 1.p=nil,表示key已删除,并且dirty中不存在数据
// 2.p=expunged,表示key已删除,dirty中存在数据且该key不在dirty中
// 3.p=&entry,表示key存在,指向一个真实的value
// 对情况1和情况3,直接将value的值存在p中,对情况2不存value,继续走后面的逻辑
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
m.mu.Lock()
// 加锁后,继续检查read中是否有key存在
read, _ = m.read.Load().(readOnly)
// key在read中,继续检查key是否已经被删除
if e, ok := read.m[key]; ok {
//2. key对应的值被标记为expunged,read中的entry拷贝到dirty时,
//会将key标记为expunged,需要手动解锁unexpungeLocked()
if e.unexpungeLocked() {
// 如果key已被删除,并且处于expunged状态,说明此key存在read但不在dirty中
// 并且此时dirty非空,需要将此key加入到dirty中,并且更新e.p的值指向value
m.dirty[key] = e
}
e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok {
// key不在read中但在dirty中,直接更新dirty中e.p的值,指向value
e.storeLocked(&value)
} else {
//走到这里说明key既不在read中也不在dirty中,肯定是一个新的key.
//并且dirty中所有的key都在read中
if !read.amended {
// 如果dirty为nil,需要创建dirty对象,并且标记read的amended为true,
// 说明有元素存在于dirty中但不在read中
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
// new一个新entry,将新值加入到dirty对象中
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
// tryStore尝试将value的值存在e.p中
func (e *entry) tryStore(i *interface{}) bool {
for {
p := atomic.LoadPointer(&e.p)
// 如果p为expunged,不能直接存储,因为此时的read中所有处于非expunged状态的key都
// 在dirty中,将key加回到read的时候,也需要将其加入到dirty中,此处不处理这种情况
// 直接返回
if p == expunged {
return false
}
// p为nil或指向&entry对象,设置e.p为i的值,即将e.p指向存入的value
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
}
}
// unexpungeLocked将e.p从expunged修改为nil
func (e *entry) unexpungeLocked() (wasExpunged bool) {
return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
// storeLocked原子操作,将i存储到e.p中,此处的i不能是expunged值
func (e *entry) storeLocked(i *interface{}) {
atomic.StorePointer(&e.p, unsafe.Pointer(i))
}
tryStore()key存在时的更新
- 在通过Store()插入数据时,如果key已经存在会调tryStore(),该函数中首先会判断key对应的值的状态,如果是expunged需要原子更新
func (e *entry) tryStore(i *interface{}) bool {
p := atomic.LoadPointer(&e.p)
// 这个entry是key对应的entry,p是key对应的值,如果p被设置为expunged,不能直接更新存储
if p == expunged {
return false
}
for {
// 原子更新
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
p = atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
}
}
dirtyLocked()将read中的数据同步到dirty
- 当添加的数据不在read并且也不在dirty时,如果dirty为nil需要新建,还有一种情况数据在dirty不在read时需要同步数据,将read中的数据同步到dirty,该函数内部会遍历获取read中的所有数据,执行tryExpungeLocked()通过原子操作给read中的数据设置expunged标记,如果不是expunged状态则添加到dirty
func (m *Map) dirtyLocked() {
// dirty != nil 说明dirty在上次read同步dirty数据后,已经有了修改了,这时候read的数据不一定准确,不能同步
if m.dirty != nil {
return
}
read, _ := m.read.Load().(readOnly)
m.dirty = make(map[interface{}]*entry, len(read.m))
//遍历获取read中的所有数据
for k, e := range read.m {
//这里调用tryExpungeLocked 来给entry,即key对应的值 设置标志位
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
//通过原子操作,给entry,key对应的值设置 expunged 标志
func (e *entry) tryExpungeLocked() (isExpunged bool) {
p := atomic.LoadPointer(&e.p)
for p == nil {
if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
return true
}
p = atomic.LoadPointer(&e.p)
}
return p == expunged
}
添加时的多种情况总结
- 在执行Store()添加数据时有多种情况:
- 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 查询
- 查看Load()获取指定键值的源码,实现思路和 Store() 方法类似,都是先从只读部分查找,如果没找到则再从 dirty 部分查找。不同之处在于,Load() 方法只涉及到读操作,并且不需要进行插入或者删除操作
- 执行"m.read.Load().(readOnly)"在只读快照中查找对应的键是否存在。如果存在,则直接返回其所对应的值
- 如果只读部分不存在通过mu调用Lock()加锁
- 加锁后会重新从只读部分中加载数据。如果找到了对应的键,则直接返回其所对应的值;
- 否则,从 dirty 部分中查找是否存在对应的键值对,如果存在,则返回其所对应的值,并解锁
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
// 先从只读部分中查找 key。
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
// 如果 key 存在于只读部分,则直接返回对应的 value。
return e.load()
}
// 加锁
m.mu.Lock()
//重新从只读部分中加载数据
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
m.mu.Unlock()
return e.load()
}
//如果readOnly中不存在,会通过dirty部分查找
if e, ok := m.dirty[key]; ok {
//如果dirty中存在,解锁
m.mu.Unlock()
//并返回,返回时需要采用乐观锁策略来确保并发安全性
return e.load()
}
// 如果都没有找到,则返回 nil。
m.mu.Unlock()
return nil, false
}
- 当通过read未查询到数据时,会执行missLocked()增加misses计数器,当misses与dirty长度相同时会把dirty转换为read,并将dirty置为空
- 问题: 为什么找到了entry.p,但是p对应的值为nil呢?答案在Delete函数中
missLocked() 累加计数与同步dirty数据到read
- 在调用Load()读取数据时,如果read未命中,并且dirty中存储数据时(有修改)时会调用missLocked()累加misses计数器,当m.misses 等于dirty长度时,直接把dirty的指针给read.m,并且设置dirty为nil,misses计数重置为0
func (m *Map) missLocked() {
// misses计数+1
m.misses++
// 如果没有达到临界值(dirty的长度),直接返回
if m.misses < len(m.dirty) {
return
}
// 将dirty字段的内容提升为read
//直接把dirty的指针给read.m,并且设置dirty为nil,
//这里也就是 Store 函数的最后会调用 m.dirtyLocked的原因
m.read.Store(readOnly{m: m.dirty})
// 清空dirty,dirty为map类型,清空方法是直接赋值nil,让GC清理掉里面的内容
m.dirty = nil
// misses计数重置为0
m.misses = 0
}
Delete 删除
- 在删除时同样还是优先检查key是否在read中,在read有两种情况
- 一种是此key只在read中,不在dirty中,将entry.p标记为expunged删除状态,方便后操作相同key直接修改read中e.p的值
- 一种是此key也存在dirty中,此时dirty中key对应的e和read中该key对应的e是同一个,这种情况将e.p设置为nil
- 如果删除的key不在read中,当前dirty又不为空,此时需要进一步确认key是否在dirty中,加锁处理,如果key在dirty中,直接调用delete将dirty中的key删除
- 可以重复调用Delete操作删除同一个key,只有第一次会标记删除,后面调用不做处理
- 查看Delete()源码,内部会调用Map上的LoadAndDelete(),在该方法中:
- 首先会通过readOnly只读快照判断key是否存在
- 如果不存在,并且 amended 标志位为 true说明存在更新,则加锁处理
- 加锁后会通过readOnly在判断一次,如果还不存在,会查询 dirty中是否包含该键值对
- 如果dirty中包含,会调用一个delete()函数,从 dirty map 中查找该键值对,并调用missLocked()等待dirty map被提升至readOnly 中
- 如果在 readOnly 或 dirty 中找到了该键值对,则调用entry上的delete()方法进行删除操作
func (m *Map) Delete(key interface{}) {
m.LoadAndDelete(key)
}
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
//通过readOnly只读快照查找键值对
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
//如果在 readOnly 段中未找到该键值对,并且 amended 标志位为 true说明存在更新,
//则加锁,查询 dirty中是否包含该键值对。
if !ok && read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
//在锁定状态下再次检查readOnly只读快照中是否存在
e, ok = read.m[key]
if !ok && read.amended {
// 如果仍未找到,并且 amended 标志位为 true,从 dirty map 中查找该键值对
e, ok = m.dirty[key]
if ok {
// 如果在 dirty中找到了该键值对,将该键值对从 dirty map 中删除
delete(m.dirty, key)
//无论该键值对是否存在,都要调用 missLocked 方法
//该方法中会对dirty 中的misses进行累加
//如果达到阈值会将dirty提升为read
m.missLocked()
}
}
// 操作结束后解锁
m.mu.Unlock()
}
if ok {
// 如果在 readOnly 或 dirty 中找到了该键值对,则调用entry上的delete()方法进行删除操作。
// 满足ok为true,read中肯定是有该key的, dirty有两种情况:
// 情况1,dirty中没有该key,因为dirty中不存,直接将read中e.p设置为nil,标记为删除状态
// 情况2,dirty中也有该key,dirty中key对应的e和read中该key对应的e是同一个,所以直接将
// read中的e.p设置为nil,其实也是将dirty中e.p也设置为nil了
return e.delete()
}
// 如果在readOnly 和 dirty map 中都未找到该键值对,则返回失败。
return nil, false
}
删除时的多种情况总结
- read中没有,且Map存在修改,则尝试删除dirty中的map中的key
- read中没有,且Map不存在修改,那就是没有这个key,无需操作
- read中有,尝试将key对应的值设置为nil,后面读取的时候就知道被删了,因为dirty中map的值跟read的map中的值指向的都是同一个地址空间,所以,修改了read也就是修改了dirty
Range 遍历
- 调用遍历时需要传入一个func(key, value interface{}) bool类型的函数, 对遍历到的每个键值对调用f进行处理,当传入的函数返回false则停止
- Map只有两种状态.被修改过和没有修改过
- 修改过:将dirty的指针交给read,read就是最新的数据了,然后遍历read的map
- 没有修改过:遍历read的map就好了
// Range 方法对sync.Map进行遍历操作,需要传入一个func(key, value interface{}) bool类型的
// 函数f,会对遍历到的键值对调用f进行处理,如果函数f返回false,对sync.Map的迭代将停止。
// Range 方法在遍历的时候会对sync.Map的元素至多访问一次,如果在执行Range操作的时候,有其他协程并发
// 的添加或删除元素,可能会导致有些元素未被遍历到。
// Range 方法是一个O(N)时间复杂度的操作,对于存在元素在dirty不在read的情况,进行了一个优化,将dirty
// 提升为read了,所以下次在进行Range的时候,直接对read进行遍历,不用加锁。
func (m *Map) Range(f func(key, value interface{}) bool) {
// 如果所有的元素都在read中,直接对read进行遍历
read, _ := m.read.Load().(readOnly)
// 确认是否有元素存在dirty中而不在read中
if read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if read.amended {
// 有元素在dirty中,对dirty进行遍历
read = readOnly{m: m.dirty}
// 进行一个优化,将dirty提升为read
m.read.Store(read)
// 将dirty提升为read之后,dirty置为nil
m.dirty = nil
// 计数器清理0
m.misses = 0
}
m.mu.Unlock()
}
// 对遍历到的每个元素,调用传入的函数f进行处理
for k, e := range read.m {
v, ok := e.load()
if !ok {
continue
}
if !f(k, v) {
break
}
}
}
LoadOrStore()存在则获取,不存在则添加
- LoadOrStore方法可以看做Load操作和Store操作的组合,如果key已经在sync.Map中,返回当前key对应的value,否则将存储传入的value值。
// LoadOrStore 可以看做Load操作和Store操作的组合,如果key已存在m中(无论是在read中还是dirty中),
// 就是只要key没有被删除,就返回当前的key对应的value值,否则将存储传入的value值,
// 第二返回参数是一个bool值,表示最后执行的是load操作还是store操作
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
// 还是优先检查read,避免加锁
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
// 尝试load和store操作,如果ok为true,表示load成功
actual, loaded, ok := e.tryLoadOrStore(value)
if ok {
return actual, loaded
}
}
m.mu.Lock()
// 双重检查
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
// e.p为expunged状态,表示m.dirty非空且dirty不存在该key
// 需要将key-value加到dirty中,这里dirty和read实际上key
// 指向的是同一个e,更新e值,dirty和read中都存有该key-value了
if e.unexpungeLocked() {
m.dirty[key] = e
}
actual, loaded, _ = e.tryLoadOrStore(value)
} else if e, ok := m.dirty[key]; ok {
// key在dirty中不在read中,
actual, loaded, _ = e.tryLoadOrStore(value)
m.missLocked()
} else {
if !read.amended {
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
actual, loaded = value, false
}
m.mu.Unlock()
return actual, loaded
}
func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, ok bool) {
p := atomic.LoadPointer(&e.p)
// key已被删除,需要进一步判断处理,这里先终止处理
if p == expunged {
return nil, false, false
}
// key存在,value值有效,直接返回之前的value值,即执行Load操作
if p != nil {
return *(*interface{})(p), true, true
}
// 走到这里说明key也是已经被删除,e.p为nil,并且dirty是空的,所以直接将i存储在e.p中即可,不用关心dirty
ic := i
for {
// 原子更新e.p的值,更新前为nil
if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
return i, false, true
}
// 进一步判断e.p是不是被别的地方已经修改为非nil了
p = atomic.LoadPointer(&e.p)
// 如果p为expunged,说明key在其他地方已经被删除了,需要进一步判断处理,这里先终止处理
if p == expunged {
return nil, false, false
}
// e.p已经被其他地方设置值了,这里直接返回已设置的值
if p != nil {
return *(*interface{})(p), true, true
}
}
}
sync.Map底层的entry
- 查看sync.Map的插入Store, 查询Load, 删除Delete底层源码,内部都会执行entry上的方法比如
- 在Store()插入时,首先会判断当前插入的key是否存在,如果不存在会调用一个newEntry()函数,将value封装为entry结构体变量,如果存在会调用entry上的tryStore()尝试更新,会调用storeLocked()方法更新value,将value保存到Map底层的dirty 或 readOnly,最后通过 CAS 原子操作更新 readOnly 部分,将 dirty 部分更新为最新的 readOnly
- 在Load()查询时,如果当前key存在readOnly或dirty中,会拿到一个entry结构体变量,调用load()采用乐观锁策略来确保并发安全的返回数据
- 在Delete()删除时,如果key存在也会先拿到对应的entry变量,通过entry上的delete()方法进行删除
- entry.tryStore()尝试更新源码: 通过循环CAS操作不断尝试将 entry 的指针指向一个新的值,实现更新
// tryStore 方法用于更新节点,返回更新是否成功的标记。
func (e *entry) tryStore(i *interface{}) bool {
// 通过循环 CAS 操作不断尝试更新 entry 的指针 p,直到成功或者发现已经被删除。
for {
p := atomic.LoadPointer(&e.p) // 获取 entry 的指针 p
if p == expunged { // 如果 p 被标记为删除,则直接返回 false 表示更新失败。
return false
}
// 使用 CAS 原子操作更新 entry 的指针 p
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true // 返回 true 表示更新成功
}
}
}
- entry.storeLocked():使用原子类修改value
func (e *entry) storeLocked(i *interface{}) {
atomic.StorePointer(&e.p, unsafe.Pointer(i))
}
- entry.load():获取value,乐观锁策略来确保并发安全的返回数据
// load 方法用于获取 entry 的指针,并将其转换为 interface{} 类型的值和一个布尔值返回。
func (e *entry) load() (value interface{}, ok bool) {
p := atomic.LoadPointer(&e.p) // 获取 entry 的指针 p
// 如果 p 指向的值为 nil 或者 expunged,则说明该 entry 已经被删除或尚未创建。
if p == nil || p == expunged {
return nil, false // 直接返回 false 表示加载失败
}
// 将 entry 的指针转换为 interface{} 类型的值并返回,同时返回一个 true 标识加载成功
return *(*interface{})(p), true
}
- entry.delete()删除源码: 通过循环 CAS 操作不断尝试更新 entry 的指针 p,直到成功或者发现已经被删除
// delete 方法用于删除节点,返回被删除节点的值及删除成功标记。
func (e *entry) delete() (value interface{}, ok bool) {
// 通过循环 CAS 操作不断尝试更新 entry 的指针 p,直到成功或者发现已经被删除。
for {
p := atomic.LoadPointer(&e.p) // 获取 entry 的指针 p
// 如果 p 为 nil 或者已经被删除,则说明该 entry 已经不存在,直接返回 false
if p == nil || p == expunged {
return nil, false
}
// 如果成功将 p 设置为 nil,说明删除成功
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return *(*interface{})(p), true // 返回被删除节点的值及删除成功标记
}
}
}
三. 总结
- sync.Map是一个支持并发安全的Map键值对映射表,提供了Store(key, value)插入,Load(key)获取,Delete(key)删除,Range()遍历等方法,了解底层首先要了解内部结构,内部包含了
- read属性,实际在操作时对应的是一个readOnly可以看为一个只读快照表,每个正在读取的 Goroutine 都会持有一个 readOnly 结构体
- dirty属性,修改数据时的一个可写表,可以用来判断当前当readOnly只读副本是否失效,并且多个 Goroutine 同时操作时,每个 Goroutine 都会持有一个 dirty 表
- mu锁属性防止并发写入时互相干扰
- 实际在readOnly与dirty中保存数据时会将value封装为entry结构体变量
- 在readOnly中还存在一个amended标识:为true时,表示dirty中存在read中没有的键值对
- 怎么保证并发安全的,可以看一下Store(key, value)插入和Load(key)获取源码
- 插入时在Store(key, value)源码中
- 首先,在readOnly只读快照中查找键是否存在,如果存在调用entry上的tryStore()方法通过cas修改值
- 如果readOnly只读快照中不存在,则通过Map中提供的mu写入锁属性调用Lock()函数加锁
- 加锁后双重检测再次通过readOnly判断是否存在,并且检查是否有其他协程在并发操作
- 如果readOnly中存在,调用entry上的storeLocked()方法修改该key的value,并且会判断当前key如果是expunged状态(表示该键所对应的值已经被删除,只有在 dirty 部分中才会出现该状态),从 dirty 中移除,并加入readOnly中
- 如果没有在readOnly中,会判断是否在dirty 中,存在则修改其 value
- 如果 key 既不在 readOnly 中,也不在 dirty 中,则调用newEntry()函数,将value封装为entry结构体变量插入到 dirty 中,如果dirty为空,还需要进行初始化,并将 readOnly 部分的 amended 标记设置为 true
- 最后释放锁,并且通过 CAS 原子操作更新 readOnly 部分,将 dirty 部分更新为最新的 readOnly
- 注意在存储value时,会将value封装为一个entry结构体,比如如果key不存在会调用newEntry()将value封装为entry结构体,如果key存在会调用entry上的storeLocked()修改
- 获取时在Load(key)源码中
- 执行"m.read.Load().(readOnly)"在只读快照中查找对应的键是否存在。如果存在,则直接返回其所对应的值
- 如果只读部分不存在通过mu调用Lock()加锁
- 加锁后会重新从只读部分中加载数据。如果找到了对应的键,则直接返回其所对应的值;
- 否则,从 dirty 部分中查找是否存在对应的键值对,如果存在,则返回其所对应的值,并解锁
- 通过Store(key, value)与Load(key)总结
- 在map内部提供了readOnly只读列表与dirty可写列表另外还有保护dirty可写列表的mu锁
- 基于读写分离的思想,在插入与读取时首先会基于readOnly读取或修改,如果readOnly中不存在,再加锁通过dirty处理
- 如果dirty处理完毕后,会将dirty提升到readOnly中
- 并且readOnly与dirty底层存储value时会将value封装成一个entry结构体变量,在增删改查时会调用entry下的大量方法,这些方法内部会通过原子类,cas等在保证性能的前提下以无锁的形式保证并发安全
- entry下的几个方法总结:
- load() 方法:用于获取 entry 的值。通过原子操作保证了其并发安全性。
- store() 方法:用于更新 entry 的指针。通过循环 CAS 操作来尝试更新 entry 的指针,通过原子操作保证了并发安全
- tryStore() 方法:与 store() 方法类似,用于更新 entry 的指针。不同之处在于,tryStore() 方法会尝试复用旧的 entry 对象,从而节省内存开销。通过循环 CAS 操作来尝试更新 entry 的指针,并通过原子操作保证了其并发安全性。
- delete() 方法:用于删除 entry,并将其指针标记为 nil。将 entry 标记为删除的目的是为了避免在删除过程中出现 race condition。通过循环 CAS 操作来尝试将 entry 的指针标记为 nil,并通过原子操作保证了其并发安全性。