谈谈go map的底层原理 | 青训营笔记

149 阅读2分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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装载桶的数量有 2B2^B
  • buckets 指向所有的桶
  • oldbuckets 如果不发生扩容一直为nil;发生扩容后会指向旧的桶
  • hash0 哈希种子
  • extra 指向溢出桶的地址;当发生溢出时,会通过nextOverflow指针去找当前可用的溢出桶
map03.PNG

bmap
bmap由四个变量组成

  • tophash:key哈希值的高8位
  • keys:该桶的keys数组,最多放8个;超过8个需要寻找溢出桶
  • elems:该桶的values数组,和keys对应
  • overflow:溢出桶;当该桶超过8,bmap.overflow会指向当前可用溢出桶,然后往溢出桶存放数据

map的读写

读过程

  1. 计算key哈希值的后B位找到桶号
  2. 计算key哈希值的高8位从该桶的tophash数组寻找-
  3. 没找到查看overflow有没有溢出桶;有就继续寻找
  4. 找到返回value;否则返回zero value
map04.png map06.png

写过程

  1. 与读过程一致;先查找插入的key有没有已经存在
  2. 存在则更新value;不存在就插入对应的bmap

map的扩容

扩容时机

  • 装载因子超过6.5(平均每个槽超过6.5个key)
  • 使用了太多溢出桶(溢出桶数量超过了普通桶)

扩容步骤

  1. 步骤一
    1. 创建一组新桶
    2. oldbuckets指向原有的桶数组
    3. buckets指向新的桶数组
    4. map标记为扩容状态
  2. 步骤二
    1. 将所有的数据从旧桶驱逐到新桶
    2. 采用渐进式驱逐
    3. 每次操作一个旧桶时,将数据从旧桶驱逐到新桶
    4. 读不发生驱逐
  3. 步骤三
    1. 所有的旧桶数据驱逐完后回收oldbuckets

驱逐细节
当需要对原来数据进行修改或者删除操作时会发生数组驱逐。

  1. 首先从旧桶寻找到要操作的数据
  2. 然后计算该数据新桶的桶号(后B位)
  3. 驱逐数据(删除旧桶数据,向新桶写入数据)

并发问题
当某key处于驱逐状态时,新协程过来读写、删除都会出现并发问题(可能读不到数据或者读了脏数据)
在高并发的情况下使用需要加锁或者使用sync.Map