map三步曲之三实际应用
之前已经介绍过了map的原理,简单了解一下map里存储的数据结构。本文主要介绍如果在map中进行操作。
一、查找
元素的查找有三种方式:
- v := m[k]:对应的- mapaccess1方法
- v,ok := m[k]:对应的- mapaccess2方法
- for k,v := range m:对应的是- mapaccessK方法
这几种方法的逻辑基本相同,mapaccessK的逻辑更简单一些。我们主要说一下mapaccess1过程。
1、初始条件
|  |  | 
判断map是否为空,或者元素个数是否为0,如果是,返回零值。
2、读写判断
|  |  | 
如果flags是hashWriting,那么表示map是正在写,会报错。不能进行并发写操作。
3、找到bucket
|  |  | 
先对key进行hash,然后根据这个hash值获取对应bucket号,和tophash值。
如果此时oldbuckets不为空,说明发生了扩容。
如果有扩容发生,老的buckets中的数据还没有搬迁到新的buckets中,所以要先在oldbuckets中查找。
4、在bucket中查找key
|  |  | 
在这个bucket及其溢出桶中查找,bucketCnt=8。
遍历桶元素,因为桶中的key是使用连续的存储空间存储,因此可以直接使用bucket+数据偏移量+keysize的大小得到k的位置,如果找到的这个位置和key相同,那么就能以相同的方法得到value。
二、赋值
如果key不存在,那么就需要在bucket对key进行赋值。
1、初始条件
|  |  | 
肯定不能对没有初始化过的map进行赋值
2、读写判断
|  |  | 
不能进行并发写
3、设置写标识
|  |  | 
这步标记map进入写状态,不允许进行读操作
4、找到bucket
|  |  | 
5、找到可以放置的位置
这里分成两种情况
- cell的tophash值和当前tophash值不等,可能会存在一个空槽位
|  |  | 
在执行删除后,可能会在前面留有空位,这里把这个空位记录下来,如果后面没有找到相同的key,就在这里插入
- cell的tophash值和当前tophash值相等,更新它
如果在这个bucket没有找到,就去它的溢出桶中查找
6、搬迁和扩容
这个在下面详细介绍
7、重置读写标识
|  |  | 
三、删除
删除map中的key :delete(m, key)
主要流程和查找流程差不多,找到key的位置,清除bucket槽位的key和value
|  |  | 
如果是指针的话,置nil,如果是值的话,清空内存
四、扩容
为了保证访问效率,在添加、修改或删除key时,都会检查是否扩容。
扩容其实就是空间换时间的过程。
map的扩容条件有两种情况:
1、判断已经达到装载因子,即元素个数 >= 桶总数 * 6.5
|  |  | 
2、判断溢出桶是否太多。
当桶总数<2^15^时,如果溢出桶>=桶总数,则认为溢出桶过多。
当桶总数>2^15^时,与2^15^比较,如果溢出桶>=2^15^,则认为溢出桶过多。
|  |  | 
在某些场景下,比如不断的增删,这样会造成溢出桶增多,但是负载因子不高,没有到达装载因子的临界值,这样就会导致桶的使用率不高,值存得稀疏,查找插入效率低。因此有了第二种情况的判断。
- 增量扩容:针对第一种情况,新建一个bucket,新的bucket大小是原来的2倍,然后oldbuckets搬迁数据到newbuckets。
- 等量扩容:针对第二种情况,不扩大容量,buckets数量不变,重新做一次搬迁数据的操作,把松散的键值对重新排列一次。
在源码中,和扩容相关的主要是hashGrow和growWork函数。
hashGrow函数并没有真正"搬迁",只是分配好新的buckets,并将老的buckets挂到oldbuckets下。
growWork函数才是真正进行“搬迁”操作,而调用growWork函数的是mapassign和mapdelete中。也就是说在插入、修改、删除key的时候,才会真正尝试进行buckets的“搬迁”工作。它们都会先检查oldbuckets是否“搬迁”完成(oldbuckets == nil),再决定是否进行搬迁工作。
|  |  | 
在搬迁过程中,我们会发现如下问题:
- 
如果是等量扩容,那么原key所在的桶和扩容后所在的桶不变,因此可以按照桶号进行搬迁。  
- 
如果是增量扩容,桶号就有可能发生变化。  
所以在evacuate中,有bucket x和bucket y的区别,其实就是增量扩容到原来的2倍,桶的数量是原来的2倍,前半桶为bucket x,后半桶为bucket y。
五、总结
1、map是哈希表实现,通过链地址法解决哈希冲突,核心数据结构是数组加链表。
2、map中定义了2^B^个桶,每个桶中能容纳8个key。根据key的不同哈希值,将其散落到不同的桶中。哈希值的低位(哈希值的后B个bit位)决定桶号,高位(哈希值的前8位)标识同一个桶中的不同key。桶中8个key是连续存储的,8个value也是连续存储的,是为了内存对齐。
3、当向桶中添加很多key,造成元素过多,超过装载因子所设定的程度,或频繁增删操作,造成溢出桶过多,会触发扩容机制。
扩容分为增量扩容和等量扩容。
- 
增量扩容,会增加桶的个数(增加一倍),把原来一个桶中的 keys 被重新分配到两个桶中。 
- 
等量扩容,不会更改桶的个数,只是会将桶中的数据变得紧凑。 不管是增量扩容还是等量扩容,都需要创建新的桶数组,并不是原地操作的。 
4、扩容过程是渐进性的,每次最多搬迁2个bucket,主要是防止扩容需要搬迁的 key 数量过多,引发性能问题。触发扩容的时机是增加了新元素, 桶搬迁的时机则发生在赋值、删除期间。
5、遍历map无序是因为map在扩容后,会发生key的搬迁,这就造成原来在一个bucket中的key,可能搬迁到其他bucket中,所以遍历map肯定不可能按原来的顺序。所以map在遍历的时候,并不是从0号bucket开始,每次都是随机值序号的bucket,再从其中随机的cell开始遍历,然后再按照桶序继续,直到遍历完成。


