目 录CONTENT

文章目录

Go | Map 底层原理

如风
2023-10-29 / 0 评论 / 0 点赞 / 81 阅读 / 1,177 字

Go | Map 底层原理

Map 底层原理

img

在go的map实现中,它的底层结构体是hmap,hmap里维护着若干个bucket数组 (即桶数组)。

Bucket数组中每个元素都是bmap结构,也即每个bucket都是bmap结构,每个桶中保存了8个kv对,如果8个满了,又来了一个key落在了这个桶里,会使用overflow连接下一个桶(溢出桶)。

GET获取数据

  1. 计算k4的hash值。由于当前主流机都是64位操作系统,所以计算结果有64个比特位

  2. 通过最后的“B”位来确定在哪号桶

  3. 根据k4对应的hash值前8位快速确定是在这个桶的哪个位置

  4. 对比key完整的hash是否匹配,如果匹配则获取对应value

  5. 如果都没有找到,就去连接的下一个溢出桶中找

PUT存放数据

  1. 通过key的hash值后“B”位确定是哪一个桶

  2. 遍历当前桶,通过key的tophash和hash值,防止key重复,然后找到第一个可以插入的位置,即空位置处存储数据。

  3. 如果当前桶元素已满,会通过overflow链接创建一个新的桶,来存储数据。

关于hash冲突:当两个不同的 key 落在同一个桶中,就是发生了哈希冲突。冲突的解决手段是采用链表法:在 桶 中,从前往后找到第一个空位进行插入。如果8个kv满了,那么当前桶就会连接到下一个溢出桶(bmap)。

扩容

由于map中不断的put和delete key

扩容实际上是一种整理,把后置位的数据整理到前面。这种情况下,元素会发生重排,但不会换桶。

2倍容量扩容 发生这种扩容时,元素会重排,可能会发生桶迁移

扩容的条件

装载因子是指当前map中,每个桶中的平均元素个数。

扩容条件1:**装载因子 > 6.5 **(源码中定义的)

扩容条件2: 溢出桶的数量过多

扩容时的细节

  1. 在我们的hmap结构中有一个oldbuckets吗,扩容刚发生时,会先将老数据存到这个里面。
  2. 每次对map进行删改操作时,会触发从oldbucket中迁移到bucket的操作【非一次性,分多次】
  3. 在扩容没有完全迁移完成之前,每次get或者put遍历数据时,都会先遍历oldbuckets,然后再遍历buckets。

Sync.Map 的底层原理

​ Go 的内建 map 是不支持并发写操作的,原因是 map 写操作不是并发安全的,当你尝试多个 Goroutine 操作同一个 map,会产生报错:fatal error: concurrent map writes

因此官方另外引入了 sync.Map 来满足并发编程中的应用。

sync.Map 的数据结构如下

type Map struct {
    // 加锁作用,保护 dirty 字段
    mu Mutex
    // 只读的数据,实际数据类型为 readOnly
    read atomic.Value
    // 最新写入的数据
    dirty map[interface{}]*entry
    // 计数器,每次需要读 dirty 则 +1
    misses int
}


type readOnly struct {
    // 内建 map
    m  map[interface{}]*entry
    // 表示 dirty 里存在 read 里没有的 key,通过该字段决定是否加锁读 dirty
    amended bool
}

sync.Map 的实现原理可概括为:

  • 通过 read 和 dirty 两个字段将读写分离,读的数据存在只读字段 read 上,将最新写入的数据则存在 dirty 字段上
  • 读取时会先查询 read,不存在再查询 dirty,写入时则只写入 dirty
  • 读取 read 并不需要加锁,而读或写 dirty 都需要加锁
  • 另外有 misses 字段来统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数则将 dirty 数据同步到 read 上
  • 对于删除数据则直接通过标记来延迟删除

通过这种读写分离的设计,解决了并发情况的写入安全,又使读取速度在大部分情况可以接近内建 map,非常适合读多写少的情况。

sync.Map 还有一些其他方法:

  • Range:遍历所有键值对,参数是回调函数
  • LoadOrStore:读取数据,若不存在则保存再读取

参考

0

评论区