Go语言的普通map由于不是线程安全的,所以很多时候也会使用sync包的Map来代替。sync.Map是线程安全的,但是也必须使用其提供的接口,接口不多,光看名字就知道其用途。先看下其中基本的结构
1. 数据结构
type Map struct {
mu Mutex // 内部互斥锁,增,改数据会用到,删除可能会用到
read atomic.Value // readOnly 包含部分数据,但是多线程读安全
dirty map[interface{}]*entry // 存放新增数据
misses int // 多次从read中读取失败会增加此计数,大于等于dirty后会出发missLocked,即拷贝全量dirty覆盖read
}
type readOnly struct {
m map[interface{}]*entry // 多线程读安全
amended bool // 为true时,表明dirty map中新插入的key-value,未同步到readonly
}
2.接口
- Load(key interface{}) (value interface{}, ok bool)
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// 从read中读取失败,并且dirty有更新,则从dirty加锁再读取一次
if !ok && read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// 从read中read miss后会出发missLocked,记录miss次数,超过dirty大小,就会强制更新read
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
// load 原子操作
return e.load()
}
- Store(key, value interface{})
func (m *Map) Store(key, value interface{}) {
// key存在于read中,即修改其值,tryStore是原子操作
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
// 不存在read,或者原子修改失败即表明此时有其他线程同时在删除该key
// 加锁重新从read读取,不信邪?
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
// update
if e.unexpungeLocked() {
// 如果已被清除,则直接修改dirty
m.dirty[key] = e
}
// 再修改read
e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok {
// 如果该key不在read而在dirty中,则直接修改dirty
// update
e.storeLocked(&value)
} else {
// insert
// amended为false,表示read是全量数据,还未进行任何修改
// amended为true,表示dirty有新增key-value
if !read.amended {
// dirtylock
// 1、 dirty map的初始化
// 2、并且将read中的值更新到dirty
// 3、将删除的key置为expunged
m.dirtyLocked()
// 单纯修改amended变量,
m.read.Store(readOnly{m: read.m, amended: true})
}
// dirty插入新值
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
-
Delete(key interface{})
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// read中不存在,amended为true表示有新值 那么就去dirty看看
if !ok && read.amended {
m.mu.Lock()
// 不信邪 再读一次
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
// read中还是没有,就直接删除dirty map吧,有没有已经不重要了,反正加锁了
delete(m.dirty, key)
}
m.mu.Unlock()
}
if ok {
// 并没有真正删除,只是把key对应value置为nil
e.delete()
}
}
3. 想到几个问题
-
为什么多线程是安全
- 内部分为两个map,read和dirty,读写分离,读取时原子操作;即使在读取时有删除操作也不影响;
- 更改和插入数据时,在内部会加锁;
-
删除key分为三步
- 如果该key存在read,则直接置为nil,不管dirty,否则直接从dirty删除(删除key和value);
- 将来触发dirtyLocked时(即插入新key-value),即从read更新dirty时,将value为nil的键对应的值修改expunged;
- 将来触发missLocked(即多次从read读取失败,必须去dirty查找的次数)时,将dirty直接拷贝覆盖read,这时才会真正释放删除的key-value;
步骤1 释放value,步骤3释放key,如果key是一些较大或重要的内存的引用,那么就可能要很久才能释放key对应的内存
-
修改可能不需要锁
- 如果该key在read中,则会直接尝试原子操作修改read中key-value;
- 否则就要加锁进行判断,去dirty中修改了;
-
插入新值肯定要加锁了
-
用什么姿势操作最合理
- 内部是读写分离,所以只读不写不改那就最好了,内部都是原子操作,贼快;
- 如果用sync.Map频繁读取一些不存在的键,但是修改比较少的话也贼快
- 内部存储的是value对应的指针,删除的时候,将read中对应value置为nil,但是这并妨碍我们使用value来存nil