Map
- 开放寻址法
- 拉链法
Go语言的map使用的是拉链法, 在
runtime.hmap中, hmap被定义为
type hmap struct {
count int
flags uint8
B uint8
nooverflow uint16
hash0 uint32
buckets unsafe.Pointer
oldbuckets unsafe.Pointer
nevacuate uintptr
extra *mapextra
}
2^B个bucket, b map is a bucket for a Go map
type bmap struct {
// bucketCntBits = 3
// bucketCnt = 1 << bucketCntBits
tophash [bucketCnt]uint8
}
map的初始化
- make
m := make(map[string]int, 10) - 字面量 元素少于25个时, 转化为简单赋值
hash := map[string]int {
"1": 2,
"3": 4,
"5", 6,
}
// 转化为
hash := make(map[string]int, 3)
hash["1"] = 2
hash["3"] = 4
hash["5"] = 6
元素多于25个时, 转化为循环赋值
hash := map[string]int {
"1" : 1
"2" : 2
...
"26": 26
}
// 转化为
hash := make(map[string]int, 26)
vstatk := []string{"1", "2", ... , "26"}
vstatv := []int{1, 2, ..., 26}
for i := 0; i < len(vstak); i++ {
hash[vstatk[i]] = vstatk[i]
}
map的访问
- 计算桶号
- 计算tophash
3. 匹配
从第二个桶中找到tophash等于0x5c的kv, 看k是不是我们想要的, 如果是则返回v.
如果碰撞, 继续线性查找, 如果该bucket也没有, 则去查找溢出桶, 如果都没有则该k不存在.
写入也类似
map扩容
当hash碰撞过多的时候, 溢出桶数量增加, 会退化成一个链表
map溢出桶太多会导致严重的性能下降
runtime.mapassign()可能会触发扩容的情况
- 装载因子超过6.5(品骏每个槽6.5个key)
- 使用了太多溢出桶(溢出桶超过了普通桶)
map的扩容类型
- 等量扩容 数据不多但是溢出桶太多了(整理)
- 翻倍扩容 数据太多了
扩容步骤
- 创建一组新桶
- oldbuckets指向原有的桶数组
- buckets指向新的桶数组
- map标记为扩容状态
- 将所有的数据从旧桶驱逐到新桶
- 采用渐进式驱逐
- 每次操作一个旧桶时, 将旧桶数据驱逐到新桶
- 读取时不进行驱逐, 只判断读取新桶还是旧桶
- 所有的旧桶驱逐完成后
- oldbuckets回收
map的并发
func main() {
m := make(map[int]int)
go func() {
for {
_ = m[1]
}
}()
go func() {
for {
m[2] = 2
}
}()
select {}
}
无法编译fatal error: concurrent map read and map write
A协程在桶中读数据时, B协程驱逐了这个桶, 那么A协程就会读到错误的数据或者找不到数据
解决方案
- 给map加锁(mutex)
- 使用sync.Map
追加"d":D
追加后的读写(misses ++ ), 先去read没有找到d, amended为true, 去dirty查找, 找到对应kv, misses ++
dirty提升,
misses = len(dirty)时, dirty提升为read的m, 置回初始状态
删除操作
- 正常删除
k置成nil后, GC会自动将v回收
- 追加后删除
后面需要提升的话, 要特殊处理
提升后被删key还需要特殊处理
不是读写分离, 而是普通读写和追加分离
总结
- Go语言使用拉链实现了hashmap
- 每一个桶中存储键哈希的前8位
- 桶超出8个数据, 就会存储到溢出桶中
- 装载系数或者溢出桶的增加, 会触发map扩容
- 扩容可能并不是增加桶数, 而是整理
- map扩容采用渐进式, 桶被操作时才会重新分配
- map才扩容的时候会有并发问题
- sync.Map使用了两个Map, 分离了扩容问题
- 不会引发扩容的操作(查 改)使用read map
- 会引发扩容的操作(新增)使用dirty map
- 读多写多, 追加少的时候性能好