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
开始遍历,然后再按照桶序继续,直到遍历完成。