go语言中的map是一种内建引用类型
map存储时key不可重复,无顺序,排序的话可以将key排序,然后取出对应value.只有可以比较的类型才可以作key,value则无限制.
go中的map采用的是哈希map
给定key后,会通过哈希算法计算一个哈希值,低B位(这里是大写的B,2^B表示当前map中bucket的数量)代表的是存在map中的哪一个bucket,高8位则是了存在bucket中的一个uint[8]数组中,高8位所在的数组indec可用来寻找对应key的index.
map可抽象为bucket结构体组成的结构体,bucket数量一开始是固定的,后期不够用之后会进行扩容,bucket内部含有数组,bucket内部数组存储的即为key和value,为8组数据,存储方式为key0,key1,key2…value0,value1,value2…形式,而不是key和value顺次存储,这样是为了防止key和vaule长度不一致时需要额外padding.key和value存储在同一个数组中.
1.map的结构:
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的数据结构
type bmap struct {
// tophash generally contains the top byte of the hash value
// for each key in this bucket. If tophash[0] < minTopHash,
// tophash[0] is a bucket evacuation state instead.
tophash [bucketCnt]uint8
// Followed by bucketCnt keys and then bucketCnt values.
// NOTE: packing all the keys together and then all the values together makes the
// code a bit more complicated than alternating key/value/key/value/... but it allows
// us to eliminate padding which would be needed for, e.g., map[int64]int8.
// Followed by an overflow pointer.
}
图中len 就是当前map的元素个数,也就是len()返回的值。也是结构体中hmap.count的值。bucket array是指数组指针,指向bucket数组。hash seed 哈希种子。overflow指向下一个bucket。
map的底层主要是由三个结构构成:
- hmap --- map的最外层的数据结构,包括了map的各种基础信息、如大小、bucket,一个大的结构体。
- mapextra --- 记录map的额外信息,hmap结构体里的extra指针指向的结构,例如overflow bucket。
- bmap --- 代表bucket,每一个bucket最多放8个kv,最后由一个overflow字段指向下一个bmap,注意key、value、overflow字段都不显示定义,而是通过maptype计算偏移获取的。
mapextra的结构如下
type mapextra struct {
// If both key and value do not contain pointers and are inline, then we mark bucket
// type as containing no pointers. This avoids scanning such maps.
// However, bmap.overflow is a pointer. In order to keep overflow buckets
// alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.
// overflow and oldoverflow are only used if key and value do not contain pointers.
// overflow contains overflow buckets for hmap.buckets.
// oldoverflow contains overflow buckets for hmap.oldbuckets.
// The indirection allows to store a pointer to the slice in hiter.
overflow *[]*bmap
oldoverflow *[]*bmap
// nextOverflow holds a pointer to a free overflow bucket.
nextOverflow *bmap
}
其中hmap.extra.nextOverflow指向的是预分配的overflow bucket,预分配的用完了那么值就变成nil。
bmap的详细结构如下
在map中出现哈希冲突时,首先以bmap为最小粒度挂载,一个bmap累积8个kv之后,就会申请一个新的bmap(overflow bucket)挂在这个bmap的后面形成链表,优先用预分配的overflow bucket,如果预分配的用完了,那么就malloc一个挂上去。这样减少对象数量,减轻管理内存的负担,利于gc。注意golang的map不会shrink,内存只会越用越多,overflow bucket中的key全删了也不会释放。
bmap中所有key存在一块,所有value存在一块,这样做方便内存对齐。当key大于128字节时,bucket的key字段存储的会是指针,指向key的实际内容;value也是一样。
hash值的高8位存储在bucket中的tophash字段。每个桶最多放8个kv对,所以tophash类型是数组[8]uint8。把高八位存储起来,这样不用完整比较key就能过滤掉不符合的key,加快查询速度。实际上当hash值的高八位小于常量minTopHash时,会加上minTopHash,区间[0, minTophash)的值用于特殊标记。查找key时,计算hash值,用hash值的高八位在tophash中查找,有tophash相等的,再去比较key值是否相同。
type typeAlg struct {
// function for hashing objects of this type
// (ptr to object, seed) -> hash
hash func(unsafe.Pointer, uintptr) uintptr
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
// tophash calculates the tophash value for hash.
func tophash(hash uintptr) uint8 {
top := uint8(hash >> (sys.PtrSize*8 - 8))
if top < minTopHash {
top += minTopHash
}
return top
}
golang为每个类型定义了类型描述器_type,并实现了hashable类型的_type.alg.hash和_type.alg.equal,以支持map的范型,定义了这类key用什么hash函数、bucket的大小、怎么比较之类的,通过这个变量来实现范型。
2. map的并发安全性
map的并发操作不是安全的。并发起两个goroutine,分别对map进行数据的增加:
sync.Map的原理:sync.Map里头有两个map一个是专门用于读的read map,另一个是才是提供读写的dirty map;优先读read map,若不存在则加锁穿透读dirty map,同时记录一个未从read map读到的计数,当计数到达一定值,就将read map用dirty map进行覆盖。
特点:官方出品,通过空间换时间的方式,读写分离;不适用于大量写的场景,会导致read map读不到数据而进一步加锁读取,同时dirty map也会一直晋升为read map,整体性能较差。适用场景:大量读,少量写。
解决方案3:分段锁
这也是数据库常用的方法,分段锁每一个读写锁保护一段区间。sync.Map其实也是相当于表级锁,只不过多读写分了两个map,本质还是一样的。
优化方向:将锁的粒度尽可能降低来提高运行速度。思路:对一个大map进行hash,其内部是n个小map,根据key来来hash确定在具体的那个小map中,这样加锁的粒度就变成1/n了。例如
3. map的GC内存回收
golang里的map是只增不减的一种数组结构,他只会在删除的时候进行打标记说明该内存空间已经empty了,不会回收。
可以看到delete是不会真正的把map释放的,所以要回收map还是需要设为nil
地址