Go 语言map深度解读

142 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

HashMap的基本实现方案

  1. 开放寻址法 image.png

    • 写:通过a经过hash找到槽,如果槽内有元素,则放入下一个槽
    • 读:通过a经过hash找到槽,如果槽内key与a不同,则向下一个槽找
  2. 拉链法image.png

    • 写:通过a经过hash找到槽,根据槽内指针插入到链表的最后一个上
    • 读: 通过a经过hash找到槽,根据槽找到桶,然后顺序遍历

Go 中的 map

在 map.go 中我们会发现 map的底层结构

image.png

image.png

buckets实际存储的是bmap结构

image.png

tophash 是一个 8位的uint组成的长度为8的数组,存储的是桶中存储键哈希的前8位

keys 与 values 是动态生成的

还有一个溢出指针

make初始化流程

image.png

创建 桶 以及溢出桶, mapextra 指向下一个可用的溢出桶

字面量初始化

image.png

image.png

map 是如何访问的

image.png

key 与 hash0 进行hash编码 低B位 就是桶号 高八位是tophash

image.png

遍历tophash 找到 与tophash 和 key相同的

image.png

如果tophash中没有 那么根据overflow 找到溢出桶继续进行遍历 如果依旧没有找到,说明没有该值

map的扩容

如果在不扩容的情况下,插入过多数据会有出现哈希碰撞。导致出现大量的溢出桶和很长的链表,大大影响性能

什么时候扩容

  1. 装载因子超过 6.5(平均每隔槽6.5个key)
  2. 使用了太多溢出桶(溢出桶超过了普通桶)

扩容的类型

  • 等量扩容(即整理桶)
  • 翻倍扩容(增加桶)

扩容

1.步骤1

  • 创建一组新的桶
  • oldbuckets指向新的桶数组
  • map标记位扩容状态

image.png 2.步骤2

  • 将所有的数据从旧桶驱逐到新桶
  • 采用渐进式驱逐
  • 每次操作旧桶时,将旧桶数据驱逐到新桶

image.png

如何解决并发问题

代码模拟并发问题

image.png

新建两个线程分别对map中两个值进行读取和修改,结果如下

image.png

报错:无法并发进行读写

正常情况下是对同一个值进行并发的读取和修改才会出现并发问题,但是为什么go语言会这么不留余地呢?

map的并发问题

  • map的读写有并发问题
  • A协程在桶中读数据时,B协程驱逐了桶
  • A协程会读到错误的数据或找不到数据

image.png

解决方案

  • 给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.
}

image.png

正常读取和修改 image.png

追加操作

  • 先read发现没有该key进入追加操作
  • 上锁 走dirty map,追加
  • 追加完成使amended = true ,即表示当前read map不是最新的了

image.png

追加后读写

  • 当追加完成后读取新追加的值时,发现read map中没有
  • 进入dirty map进行读写操作,并且使misses + 1

image.png

当 misses的大小等于 len(dirty) 时进行 dirty提升

image.png

在进行追加时会进行重建

image.png

sync.Map 的删除

  • 相比于查询、修改、新增,删除会更加麻烦
  • 删除可分为正常删除和追加后删除
  • 提升后,被删key还需特殊处理

正常删除会将指针变成 nil 这样值就会被gc回收掉 image.png

image.png

image.png

image.png

sync 实现了读写和追加进行分离