结构
Go 语言采用的是哈希查找表,并且使用链表解决哈希冲突。
type hmap struct { // 元素个数,调用 len(map) 时,直接返回此值 count int flags uint8 // buckets 的对数 log_2 B uint8 // overflow 的 bucket 近似数 noverflow uint16 // 计算 key 的哈希的时候会传入哈希函数 hash0 uint32 // 指向 buckets 数组,大小为 2^B // 如果元素个数为0,就为 nil buckets unsafe.Pointer // 扩容的时候,buckets 长度会是 oldbuckets 的两倍 oldbuckets unsafe.Pointer // 指示扩容进度,小于此地址的 buckets 迁移完成 nevacuate uintptr extra *mapextra // optional fields }
tophash:记录哈希值高8位,为了快速遍历。
通过make初始化会调用runtime.makemap(B)。从数值B决定了要创建多少个桶例如B是3则创建8个正常桶。如下图,会多创建几个溢出痛,由extra的overflow指向下一个溢出桶。
通过自变量初始化。如下图两种赋值方式
map的访问 :
计算桶号,确定哪个bmap
计算tophash:,得到tophash后去刚才得到的桶里遍历得到tophash的项,再看看key是否是我们想要的key。
如果没找到则由overflow去溢出桶里再去遍历。
map扩容:
原因:溢出桶太多会扩容,2 装在因子超过6.5。
1,等量扩容。数据曾经很多现在都被删了,所以等量扩容整理整理。
2,翻倍扩容。
扩容步骤1,创建一组新桶。2,oldbuckets指向原有的桶数组。3 buckets指向新的桶数组。4 map标记扩容状态。
步骤2将所有数据从旧桶驱逐到新桶。渐进式驱逐。每操作一个旧桶,将旧桶数据驱逐到新桶。如下图例子,老的2号桶的数据将被分到新的2号桶和6号桶。
步骤3。老桶释放
map的并发问题
map不能读写并发。原因:A协程读老桶的数据,B协程驱逐了这个桶把这个桶的数据驱逐到新桶中,则A读到错误的数据。解决方案: 1: 给map加mutex锁。2: 使用sync.map。
sync.map的读写修改
这里key分别是 a b c。value对应 A B C。正常情况下的数据结构如下:
正常读写走下面这条线:
追加操作:1,先正常读read到m,发现没有d于是回来上锁。锁的是dirty map。对dirty map追加。追加后dirtymap 多了个d,并且read map amended=true表示有追加。
追加后的读写:1,先正常走read map 找不到,发现amended为true,于是去dirty map读。读完miss+=1,表示read map 没有 dirty map 有。当misses==dirty时候,会dirty提升。将原来read map删除。
再重建:
sync.map的删除。(正常删除,追加后删除,提升后被删的key的处理)
1,删除d D 将指针设置为nil
后面由GO的GC垃圾自动回收
2,追加删除。删除D d .同样先找read map发现没有,再找dirty map。发现在下面,设置为nil。
重建: