携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情
HashMap的基本实现方案
-
开放寻址法
- 写:通过a经过hash找到槽,如果槽内有元素,则放入下一个槽
- 读:通过a经过hash找到槽,如果槽内key与a不同,则向下一个槽找
-
拉链法
- 写:通过a经过hash找到槽,根据槽内指针插入到链表的最后一个上
- 读: 通过a经过hash找到槽,根据槽找到桶,然后顺序遍历
Go 中的 map
在 map.go 中我们会发现 map的底层结构
buckets实际存储的是bmap结构
tophash 是一个 8位的uint组成的长度为8的数组,存储的是桶中存储键哈希的前8位
keys 与 values 是动态生成的
还有一个溢出指针
make初始化流程
创建 桶 以及溢出桶, mapextra 指向下一个可用的溢出桶
字面量初始化
map 是如何访问的
key 与 hash0 进行hash编码 低B位 就是桶号 高八位是tophash
遍历tophash 找到 与tophash 和 key相同的
如果tophash中没有 那么根据overflow 找到溢出桶继续进行遍历 如果依旧没有找到,说明没有该值
map的扩容
如果在不扩容的情况下,插入过多数据会有出现哈希碰撞。导致出现大量的溢出桶和很长的链表,大大影响性能
什么时候扩容
- 装载因子超过 6.5(平均每隔槽6.5个key)
- 使用了太多溢出桶(溢出桶超过了普通桶)
扩容的类型
- 等量扩容(即整理桶)
- 翻倍扩容(增加桶)
扩容
1.步骤1
- 创建一组新的桶
- oldbuckets指向新的桶数组
- map标记位扩容状态
2.步骤2
- 将所有的数据从旧桶驱逐到新桶
- 采用渐进式驱逐
- 每次操作旧桶时,将旧桶数据驱逐到新桶
如何解决并发问题
代码模拟并发问题
新建两个线程分别对map中两个值进行读取和修改,结果如下
报错:无法并发进行读写
正常情况下是对同一个值进行并发的读取和修改才会出现并发问题,但是为什么go语言会这么不留余地呢?
map的并发问题
- map的读写有并发问题
- A协程在桶中读数据时,B协程驱逐了桶
- A协程会读到错误的数据或找不到数据
解决方案
- 给map加锁(mutex)
- 使用sync.Map(可并发读写,性能损失可控)
找到sync.map 的源码我们会发现结构体如下
type Map struct {
mu Mutex //锁
read atomic.Value //下方的readOnly
dirty map[interface{}]*entry //键和值都是万能类型
misses int //未命中
}
type readOnly struct {
m map[interface{}]*entry //键和值都是万能类型
amended bool // true if the dirty map contains some key not in m.
}
正常读取和修改
追加操作
- 先read发现没有该key进入追加操作
- 上锁 走dirty map,追加
- 追加完成使amended = true ,即表示当前read map不是最新的了
追加后读写
- 当追加完成后读取新追加的值时,发现read map中没有
- 进入dirty map进行读写操作,并且使misses + 1
当 misses的大小等于 len(dirty) 时进行 dirty提升
在进行追加时会进行重建
sync.Map 的删除
- 相比于查询、修改、新增,删除会更加麻烦
- 删除可分为正常删除和追加后删除
- 提升后,被删key还需特殊处理
正常删除会将指针变成 nil 这样值就会被gc回收掉
sync 实现了读写和追加进行分离