GO中map底层详解

本文详细解析了Go语言Map的数据结构(hmap和bmap),包括其底层实现、查找和插入过程,以及扩容策略(等量扩容和增量扩容)。还讨论了并发安全问题及如何在并发场景中正确使用Map或sync.Map。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 map的数据结构

map其实是一种key-value的键值对存储结构,其中key不能重复,底层是通过使用hash表进行存储的。

源码:

 type hmap struct {
     count     int    // 元素的个数
     B         uint8  // buckets 数组的长度就是 2^B 个
     overflow uint16 // 溢出桶的数量
 ​
     buckets    unsafe.Pointer // 2^B个桶对应的数组指针
     oldbuckets unsafe.Pointer  // 发生扩容时,记录扩容前的buckets数组指针
 ​
     extra *mapextra //用于保存溢出桶的地址
 }
 ​
 type mapextra struct {
     overflow    *[]*bmap
     oldoverflow *[]*bmap
 ​
     nextOverflow *bmap
 }
 ​
 type bmap struct {
     tophash [bucketCnt]uint8
 }
 ​
 //在编译期间会产生新的结构体
 type bmap struct {
     tophash [8]uint8 //存储哈希值的高8位
     data    byte[1]  //key value数据:key/key/key/.../value/value/value...
     overflow *bmap   //溢出bucket的地址
 }
 

下图是对它的一个解释

由图加上面的源码我们不难看出,其实map的底层结构体是hmap,同时hmap里面维护着若干个bucket数组(即桶数组)。bucket数组中每个元素都是bmap结构的,bmap中存储着8个key-value的键值对,如果是满了的话,当再来一个键值对的时候就会放到下一个溢出桶中,同时是通过这个overflow进行连接。如果当溢出桶过多的时候可能会发生扩容的操作,在扩容的时候会通过渐进式的方式进行扩容,oldbuckets指向原来的桶。

二 map的操作过程

1.查找过程

1.首先根据key值计算出哈希值

2.取哈希值的低八位与hmap.B取模确定bucket位置

3.取高八位在桶数组中进行查找

4.如果tophash[i]中存储值也哈希值相等,则去找到该bucket中的key值进行比较

5.当前bucket没有找到,则继续从下个overflow的bucket中查找。

6.如果当前处于搬迁过程,则优先从oldbuckets查找

2.插入的过程

1.根据key值算出哈希值

2.取哈希值低位与hmap.B取模确定bucket位置

3.查找该key是否已经存在,如果存在则直接更新值

4.如果没找到将key,将key插入

三 map的扩容操作

map的扩容方式有两种,一种是等量扩容,一种是增量扩容

1.等量扩容

时机:溢出桶过多:由于map中不断的put和delete key,桶中可能会出现很多断断续续的空位,这些空位会导致连接的bmap溢出桶很长,导致扫描时间边长。

操作:申请和原来等量的内容,将原来的数据重新整理后,写入到新的内存中。可以简单的认为是一次内存整理,目的是提高查询效率。

引申,如果没有等量扩容会出现什么问题?随着溢出桶缓慢增长,有内存溢出的风险。

2.增量扩容

时机:装载因子(键值对/桶)>6.5

操作:增量扩容 分成两步: 第一步进入扩容状态,先申请一块新的内存,翻倍增加桶的数量,此时buckets指向新分配的桶,oldbuckets指向原来的桶。 第二步,重新计算老的桶中的哈希值在新的桶内的位置(取模或者位操作),将旧数据用渐进式的方式拷贝到新的桶中。

渐进式迁移分两块,一方面会从第一个桶开始,顺序迁移每一个桶,如果下一个桶已经迁移,则跳过。另一方面,当我们操作某一个桶的元素时,会迁移两个桶,进而保证经过一些操作后一定能够完成迁移。

当我们访问一个正在迁移的Map时,如果存在oldbuckets,那么直接去中oldbuckets寻找数据。当我们遍历一个正在迁移的Map时,新的和旧的就会遍历,如果一个旧的的桶已经迁移走了,那么就直接跳过,反正不在旧的就在新的里。Map遍历本身就是无序的

引申,如何实现map的有序遍历?

1.使用切片,将Key先排好序,再遍历逐个去Map中取Value。

2.包装一个新的数据结构。链表,或者其他结构。

四 问题引申

1.如何安全的使用map?

Map本身是不安全的,非原子操作的。多个GoRoutine并发操作Map轻则数据混乱,重则直接panic。如果要在并发中使用Map,要么自己加读写锁sync.RWMutex,要么使用sync.Map

2.map加锁和使用sync.Map那种好?

map加锁,在并发访问频繁的情况下,由于锁的竞争,可能会造成性能瓶颈。

sync.Map通过分段锁实现并发安全的。

分段锁sync.Map内部使用了一种分段锁的机制,将整个映射分为若干个段(segments)。每个段都是一个小的哈希表,每个段有自己的锁来保护对该段的访问。这样做的好处是在大多数情况下,只需要锁住其中的一部分数据,而不是整个映射,从而提高了并发性能。

无锁读取:在进行读取操作时,不需要获取任何锁。每个段的读取操作都是并发安全的,因为读取操作不会修改数据,所以不需要锁定。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值