哈希表原理
其实map
是一种HashMap
,表面上看它只有键值对结构,实际上在存储键值对的过程中涉及到了数组和链表。HashMap
之所以高效,是因为其结合了顺序存储(数组)和链式存储(链表)两种存储结构。数组是HashMap
的主干,在数组下有有一个类型为链表的元素。
当我们存储一个键值对时,HashMap
会首先通过一个哈希函数将key
转换为数组下标,真正的key-value
是存储在该数组对应的链表里。
当发生哈希碰撞时,键值对就会存储在该数组对应链表的下一个节点上。
HashMap
的操作效率也是很高的。当不存在哈希碰撞时查找复杂度为O(1)
,存在哈希碰撞时复杂度为O(N)
。
计算key的哈希值
哈希函数的选取应尽可能使新增的键值对均匀地分布在数组里。
golang map
go中map结构
map
的底层结构是hmap
(即hashmap的缩写),核心元素是一个由若干个桶(bucket
,结构为bmap
)组成的数组,每个bucket
可以存放若干元素(通常是8个),key通过哈希算法被归入不同的bucket
中。当超过8个元素需要存入某个bucket
时,hmap
会使用extra
中的overflow
来拓展该bucket
。
hmap
的结构体如下:
type hmap struct {
count int // # 元素个数
flags uint8
B uint8 // 说明包含2^B个bucket
noverflow uint16 // 溢出的bucket的个数
hash0 uint32 // hash种子
buckets unsafe.Pointer // buckets的数组指针
oldbuckets unsafe.Pointer // 结构扩容的时候用于复制的buckets数组
nevacuate uintptr // 搬迁进度(已经搬迁的buckets数量)
extra *mapextra
}
bucket(bmap)
的结构如下
type bmap struct {
tophash [bucketCnt]uint8
}
tophash
用于记录8个key哈希值的高8位,这样在寻找对应key的时候可以更快,不必每次都对key做全等判断。bucket
并非只有一个tophash
,而是后面紧跟8组kv
对和一个overflow
的指针,这样才能使overflow
成为一个链表的结构。但是这两个结构体并不是显示定义的,而是直接通过指针运算进行访问的。kv
的存储形式为key0key1key2key3…key7val1val2val3…val7
,这样做的好处是:在key
和value
的长度不同的时候,节省padding
空间。如上面的例子,在map[int64]int8
中,4个相邻的int8
可以存储在同一个内存单元中。如果使用kv
交错存储的话,每个int8
都会被padding
占用单独的内存单元(为了提高寻址速度)。
map存储值
首先用key的hash
值低8位找到bucket
,然后在bucket
内部比对tophash
和高8位与其对应的key
值与入参key
是否相等,若找到则更新这个值。若key
不存在,则key
优先存入在查找的过程中遇到的空的tophash
数组位置。若当前的bucket
已满则需要另外分配空间给这个key
,新分配的bucket
将挂在overflow
链表后。
map的增长
随着元素的增加,在一个bucket
链中寻找特定的key
会变得效率低下,所以在插入的元素个数/bucket
个数达到某个阈值(当前设置为6.5
,实验得来的值)时,map
会进行扩容,代码中详见 hashGrow
函数。首先创建bucket
数组,长度为原长度的两倍
newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger)
,然后替换原有的bucket
,原有的bucket
被移动到oldbucket
指针下。
扩容完成后,每个hash
对应两个bucket
(一个新的一个旧的)。oldbucket
不会立即被转移到新的bucket
下,而是当访问到该bucket
时,会调用growWork
方法进行迁移,growWork
方法会将oldbucket
下的元素rehash
到新的bucket
中。随着访问的进行,所有oldbucket
会被逐渐移动到bucket
中。
但是这里有个问题:如果需要进行扩容的时候,上一次扩容后的迁移还没结束,怎么办?在代码中我们可以看到很多again
标记,会不断进行迁移,知道迁移完成后才会进行下一次扩容。
但这个迁移并没有在扩容之后一次性完成,而是逐步完成的,每一次insert
或remove
时迁移1到2个pair
,即增量扩容。增量扩容的原因主要是缩短map
容器的响应时间。若hashmap
很大扩容时很容易导致系统停顿无响应。增量扩容本质上就是将总的扩容时间分摊到了每一次hash
操作上。由于这个工作是逐渐完成的,导致数据一部分在old table
中一部分在new table
中。old
的bucket
不会删除,只是加上一个已删除的标记。只有当所有的bucket
都从old table
里迁移后才会将其释放掉。
map中注意点
-
删除掉map中的元素是否会释放内存?
不会,删除操作仅仅将对应的tophash[i]设置为empty,并非释放内存。若要释放内存只能等待指针无引用后被系统gc
-
如何并发地使用map?
map不是goroutine安全的,所以在有多个gorountine对map进行写操作是会panic。多gorountine读写map是应加锁(RWMutex),或使用sync.Map
-
map的iterator是否安全?
map的delete并非真的delete,所以对迭代器是没有影响的,是安全的。
转自:<https://blog.yiz96.com/golang-map/>