这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
在项目中我们经常需要用到map这种数据结构。map本身是并发不安全的
下面来聊聊map的底层原理,并说明它为什么并发不安全。
map的底层结构
hmap
map的底层结构体是hmap
type hmap struct {
//一些重要字段
count int
flags uint8
B uint8
buckets unsafe.Pointer
oldbuckets unsafe.Pointer
hash0 uint32
extra *mapextra
}
- count 当前已插入键值对的数量
- flags 扩容标记
- B 表示当前buckets装载桶的数量有 个
- buckets 指向所有的桶
- oldbuckets 如果不发生扩容一直为nil;发生扩容后会指向旧的桶
- hash0 哈希种子
- extra 指向溢出桶的地址;当发生溢出时,会通过nextOverflow指针去找当前可用的溢出桶
bmap
bmap由四个变量组成
- tophash:key哈希值的高8位
- keys:该桶的keys数组,最多放8个;超过8个需要寻找溢出桶
- elems:该桶的values数组,和keys对应
- overflow:溢出桶;当该桶超过8,bmap.overflow会指向当前可用溢出桶,然后往溢出桶存放数据
map的读写
读过程
- 计算key哈希值的后B位找到桶号
- 计算key哈希值的高8位从该桶的tophash数组寻找-
- 没找到查看overflow有没有溢出桶;有就继续寻找
- 找到返回value;否则返回zero value
写过程
- 与读过程一致;先查找插入的key有没有已经存在
- 存在则更新value;不存在就插入对应的bmap
map的扩容
扩容时机
- 装载因子超过6.5(平均每个槽超过6.5个key)
- 使用了太多溢出桶(溢出桶数量超过了普通桶)
扩容步骤
- 步骤一
- 创建一组新桶
- oldbuckets指向原有的桶数组
- buckets指向新的桶数组
- map标记为扩容状态
- 步骤二
- 将所有的数据从旧桶驱逐到新桶
- 采用渐进式驱逐
- 每次操作一个旧桶时,将数据从旧桶驱逐到新桶
- 读不发生驱逐
- 步骤三
- 所有的旧桶数据驱逐完后回收oldbuckets
驱逐细节
当需要对原来数据进行修改或者删除操作时会发生数组驱逐。
- 首先从旧桶寻找到要操作的数据
- 然后计算该数据新桶的桶号(后B位)
- 驱逐数据(删除旧桶数据,向新桶写入数据)
并发问题
当某key处于驱逐状态时,新协程过来读写、删除都会出现并发问题(可能读不到数据或者读了脏数据)
在高并发的情况下使用需要加锁或者使用sync.Map