Golang - Map 内部实现原理解析

Golang - Map 内部实现原理解析

一.前言

  • Golang中Map存储的是kv键值对,采用哈希表作为底层实现,用拉链法解决hash冲突

本文Go版本:gov1.14.4,源码位于src/runtime/map.go

二.Map的内存模型

在源码中,表示map的结构体是hmap,是hashmap的缩写

const (
	// 一个桶(bucket)内 可容纳kv键值对 的最大数量
	bucketCntBits = 3
	bucketCnt     = 1 << bucketCntBits
)

// map的底层结构
type hmap struct {
	count     int    // map中kv键值对的数量
	flags     uint8  // 状态标识符,比如正在被写,buckets和oldbuckets正在被遍历或扩容
	B         uint8  // 2^B=len(buckets)
	noverflow uint16 // 溢出桶的大概数量,当B小于16时是准确值,大于等于16时是大概的值
	hash0     uint32 // hash因子

	buckets    unsafe.Pointer // 指针,指向一个[]bmap类型的数组,数组大小为2^B,我们将一个bmap叫做一个桶,buckets字段我们称之为正常桶,正常桶存满8个元素后,正常桶指向的下一个桶,我们将其叫做溢出桶(拉链法)
	oldbuckets unsafe.Pointer // 类型同上,用途不同,用于在扩容时存放之前的buckets
	nevacuate  uintptr        // 计数器,表示扩容进度

	extra *mapextra // 用于gc,指向所有的溢出桶,正常桶里面某个bmap存满了,会使用这里面的内存空间存放键值对
}

// 溢出桶结构
type mapextra struct {
	overflow    *[]*bmap // 指针数组,指向所有溢出桶
	oldoverflow *[]*bmap // 指针数组,发生扩容时,指向所有旧的溢出桶

	nextOverflow *bmap // 指向 所有溢出桶中 下一个可以使用的溢出桶
}

// 桶结构
type bmap struct {
	tophash  [bucketCnt]uint8     // 存放key哈希值的高8位,用于决定kv键值对放在桶内的哪个位置

	// 以下属性,编译时动态生成,在源码中不存在
	keys     [bucketCnt]keytype   // 存放key的数组
	values   [bucketCnt]valuetype // 存放value的数组
	pad      uintptr              // 用于对齐内存
	overflow uintptr              // 指向下一个桶,即溢出桶,拉链法
}

用图表示一下map底层的内存模型:

解析:

  • map的内存模型中,其实总共就三种结构,hmap,bmap,mapextra

  • hmap表示整个map,bmap表示hmap中的一个桶,map底层其实是由很多个桶组成的

  • 当一个桶存满之后,指向的下一个桶,就叫做溢出桶,溢出桶就是拉链法的具体表现

  • mapextra表示所有的溢出桶,之所以还要重新的指向,目的是为了用于gc,避免gc时扫描整个map,仅扫描所有溢出桶就足够了

  • 桶结构的很多字段得在编译时才会动态生成,比如key和values等

  • 桶结构中,之所以所有的key放一起,所有的value放一起,而不是key/value一对对的一起存放,目的便是在某些情况下可以省去pad字段,节省内存空间

  • golang中的map使用的内存是不会收缩的,只会越用越多。

三.Map的设计原理

1.hash值的使用

通过哈希函数,key可以得到一个唯一值,map将这个唯一值,分成高8位和低8位,分别有不同的用途

  • 低8位:用于寻找当前key属于哪个bucket
  • 高8位:用于寻找当前key在bucket中的位置,bucket有个tohash字段,便是存储的高8位的值,用来声明当前bucket中有哪些key,这样搜索查找时就不用遍历bucket中的每个key,只要先看看tohash数组值即可,提高搜索查找效率

map其使用的hash算法会根据硬件选择,比如如果cpu是否支持aes,那么采用aes哈希,并且将hash值映射到bucket时,会采用位运算来规避mod的开销

2.桶的细节设计

bmap结构,即桶,是map中最重要的底层实现之一,其设计要点如下:

  • 桶是map中最小的挂载粒度:map中不是每一个key都申请一个结构通过链表串联,而是每8个kv键值对存放在一个桶中,然后桶再通以链表的形式串联起来,这样做的原因就是减少对象的数量,减轻gc的负担。

  • 桶串联实现拉链法:当某个桶数量满了,会申请一个新桶,挂在这个桶后面形成链表,新桶优先使用预分配的桶。

  • 哈希高8位优化桶查找key : 将key哈希值的高8位存储在桶的tohash数组中,这样查找时不用比较完整的key就能过滤掉不符合要求的key,tohash中的值相等,再去比较key值

  • 桶中key/value分开存放 : 桶中所有的key存一起,所有的value存一起,目的是为了方便内存对齐

  • 根据k/v大小存储不同值 : 当k或v大于128字节时,其存储的字段为指针,指向k或v的实际内容,小于等于128字节,其存储的字段为原值

  • 桶的搬迁状态 : 可以根据tohash字段的值,是否小于minTopHash,来表示桶是否处于搬迁状态

3.map的扩容与搬迁策略

map底层扩容策略如下:

  • map的扩容策略是新分配一个更大的数组,然后在插入和删除key的时候,将对应桶中的数据迁移到新分配的桶中去

map的搬迁策略如下:

  • 由于map扩容需要将原有的kv键值对搬迁到新的内存地址,直接一下子全部搬完会非常的影响性能
  • 采用渐进式的搬迁策略,将搬迁的O(N)开销均摊到O(1)的赋值和删除操作上

以下两种情况时,会进行扩容:

  • 当装载因子超过6.5时,扩容一倍,属于增量扩容

  • 当使用的溢出桶过多时间,重新分配一样大的内存空间,属于等量扩容,实际上没有扩容,主要是为了回收空闲的溢出桶

装载因子等于 map中元素的个数 / map的容量,即len(map) / 2^B

  • 装载因子用来表示空闲位置的情况,装载因子越大,表明空闲位置越少,冲突也越多
  • 随着装载因子的增大,哈希表线性探测的平均用时就会增加,这会影响哈希表的性能,当装载因子大于70%,哈希表的性能就会急剧下降,当装载因子达到100%,整个哈希表就会完全失效,这个时候,查找和插入任意元素的复杂度都是O(N),因为需要遍历所有元素.

为什么会出现以上两种情况?

  • 情况1:确实是数据量越来越多,撑不住了

  • 情况2:比较特殊,归根结底还是map删除的特性导致的,当我们不断向哈希表中插入数据,并且将他们又全部删除时,其内存占用并不会减少,因为删除只是将桶对应位置的tohash置nil而已,这种情况下,就会不断的积累溢出桶造成内存泄露。为了解决这种情况,采用了等量扩容的机制,一旦哈希表中出现了过多的溢出桶,她会创建新桶保存数据,gc会清理掉老的溢出桶,从而避免内存泄露。

如何定义溢出桶是否太多需要等量扩容呢?两种情况:

  • 当B小于15时,溢出桶的数量超过2^B,属于溢出桶数量太多,需要等量扩容
  • 当B大于等于15时,溢出桶数量超过2^B,属于溢出桶数量太多,需要等量扩容

4.map泛型的实现

  • golang并没有实现泛型,为了支持map的泛型,底层定义了一个maptype类型,maptype定义了这类key使用什么hash函数,定义了bucket的大小,bucket如何比较。
type maptype struct {
	typ        _type
	key        *_type                                // key类型
	elem       *_type                                // value类型
	bucket     *_type                                // 桶内部使用的类型
	hasher     func(unsafe.Pointer, uintptr) uintptr // 哈希函数
	keysize    uint8                                 // key大小
	elemsize   uint8                                 // value大小
	bucketsize uint16                                // bucket大小
	flags      uint32
}

四.Map的源码实现

1.创建map

  • 创建map,主要是创建hmap这个结构,以及对hmap的初始化
// 创建map
func makemap(t *maptype, hint int, h *hmap) *hmap {
	// 参数校验,计算哈希占用的内存是否溢出或者超出能分配的最大值
	mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
	if overflow || mem > maxAlloc {
		hint = 0
	}

	// 初始化 hmap
	if h == nil {
		h = new(hmap)
	}
	// 获取一个随机的哈希种子
	h.hash0 = fastrand()

	// 确定B的大小
	B := uint8(0)
	for overLoadFactor(hint, B) {
		B++
	}
	h.B = B

	// 分配桶
	if h.B != 0 {
		var nextOverflow *bmap
		h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
		if nextOverflow != nil {
			h.extra = new(mapextra)
			h.extra.nextOverflow = nextOverflow
		}
	}

	return h
}

makeBucketArray函数是给buckets字段分配桶空间的,知道大致功能就ok了

  • 默认会创建2^B个bucket,如果b大于等于4,会预先创建一些溢出桶,b小于4的情况可能用不到溢出桶,没必要预先创建

2.map中赋值元素

  • mapassign函数,从非常宏观的角度,抛开并发安全和扩容等操作不谈,大致可以分成下面五个步骤
// 往map中添加元素/修改元素值
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
	if h == nil {
		panic(plainError("assignment to entry in nil map"))
	}
	if raceenabled {
		callerpc := getcallerpc()
		pc := funcPC(mapassign)
		racewritepc(unsafe.Pointer(h), callerpc, pc)
		raceReadObjectPC(t.key, key, callerpc, pc)
	}
	if msanenabled {
		msanread(key, t.key.size)
	}
	if h.flags&hashWriting != 0 {
		throw("concurrent map writes")
	}
	// 第一部分: 确认哈希值
	hash := t.hasher(key, uintptr(h.hash0))
	
	h.flags ^= hashWriting

	if h.buckets == nil {
		h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
	}

again:
	
	// 第二部分: 根据hash值确认key所属的桶
	bucket := hash & bucketMask(h.B)
	if h.growing() {
		growWork(t, h, bucket)
	}
	b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
	top := tophash(hash)

	var inserti *uint8
	var insertk unsafe.Pointer
	var elem unsafe.Pointer
bucketloop:
	
	// 第三部分: 遍历所属桶和此桶串联的溢出桶,寻找key(通过桶的tohash字段和key值)
	for {
		for i := uintptr(0); i < bucketCnt; i++ {
			if b.tophash[i] != top {
				if isEmpty(b.tophash[i]) && inserti == nil {
					inserti = &b.tophash[i]
					insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
					elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
				}
				if b.tophash[i] == emptyRest {
					break bucketloop
				}
				continue
			}
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
			if t.indirectkey() {
				k = *((*unsafe.Pointer)(k))
			}
			if !t.key.equal(key, k) {
				continue
			}
			if t.needkeyupdate() {
				typedmemmove(t.key, k, key)
			}
			elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
			goto done
		}
		ovf := b.overflow(t)
		if ovf == nil {
			break
		}
		b = ovf
	}


	if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
		hashGrow(t, h)
		goto again 
	}

	// 第四部分: 当前链上所有桶都满了,创建一个新的溢出桶,串联在末尾,然后更新相关字段
	if inserti == nil {
		newb := h.newoverflow(t, b)
		inserti = &newb.tophash[0]
		insertk = add(unsafe.Pointer(newb), dataOffset)
		elem = add(insertk, bucketCnt*uintptr(t.keysize))
	}

	// 第五部分 根据key是否存在,在桶中更新或者新增key/value值
	if t.indirectkey() {
		kmem := newobject(t.key)
		*(*unsafe.Pointer)(insertk) = kmem
		insertk = kmem
	}
	if t.indirectelem() {
		vmem := newobject(t.elem)
		*(*unsafe.Pointer)(elem) = vmem
	}
	typedmemmove(t.key, insertk, key)
	*inserti = top
	h.count++

done:
	if h.flags&hashWriting == 0 {
		throw("concurrent map writes")
	}
	h.flags &^= hashWriting
	if t.indirectelem() {
		elem = *((*unsafe.Pointer)(elem))
	}
	return elem
}

3.map中删除元素

  • mapdelete函数,大致可以分为以下六步
// map中删除元素
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
	if raceenabled && h != nil {
		callerpc := getcallerpc()
		pc := funcPC(mapdelete)
		racewritepc(unsafe.Pointer(h), callerpc, pc)
		raceReadObjectPC(t.key, key, callerpc, pc)
	}
	if msanenabled && h != nil {
		msanread(key, t.key.size)
	}
	if h == nil || h.count == 0 {
		if t.hashMightPanic() {
			t.hasher(key, 0) // see issue 23734
		}
		return
	}

	// 第一部分: 写保护
	if h.flags&hashWriting != 0 {
		throw("concurrent map writes")
	}

	// 第二部分: 获取hash值
	hash := t.hasher(key, uintptr(h.hash0))

	// Set hashWriting after calling t.hasher, since t.hasher may panic,
	// in which case we have not actually done a write (delete).
	h.flags ^= hashWriting

	// 第三部分: 根据hash值确定桶,并看是否需要扩容
	bucket := hash & bucketMask(h.B)
	if h.growing() {
		growWork(t, h, bucket)
	}
	b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
	bOrig := b
	top := tophash(hash)

	// 第四部分:遍历桶和桶串联的溢出桶
search:
	for ; b != nil; b = b.overflow(t) {
		for i := uintptr(0); i < bucketCnt; i++ {
			if b.tophash[i] != top {
				// 快速试错
				if b.tophash[i] == emptyRest {
					break search
				}
				continue
			}

			// 第五部分: 找到key,然后将桶的该key的tohash值置空,相当于删除值了
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
			k2 := k
			if t.indirectkey() {
				k2 = *((*unsafe.Pointer)(k2))
			}
			if !t.key.equal(key, k2) {
				continue
			}
			// Only clear key if there are pointers in it.
			if t.indirectkey() {
				*(*unsafe.Pointer)(k) = nil
			} else if t.key.ptrdata != 0 {
				memclrHasPointers(k, t.key.size)
			}
			e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
			if t.indirectelem() {
				*(*unsafe.Pointer)(e) = nil
			} else if t.elem.ptrdata != 0 {
				memclrHasPointers(e, t.elem.size)
			} else {
				memclrNoHeapPointers(e, t.elem.size)
			}
			b.tophash[i] = emptyOne
			// If the bucket now ends in a bunch of emptyOne states,
			// change those to emptyRest states.
			// It would be nice to make this a separate function, but
			// for loops are not currently inlineable.
			if i == bucketCnt-1 {
				if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {
					goto notLast
				}
			} else {
				if b.tophash[i+1] != emptyRest {
					goto notLast
				}
			}
			for {
				b.tophash[i] = emptyRest
				if i == 0 {
					if b == bOrig {
						break // beginning of initial bucket, we're done.
					}
					// Find previous bucket, continue at its last entry.
					c := b
					for b = bOrig; b.overflow(t) != c; b = b.overflow(t) {
					}
					i = bucketCnt - 1
				} else {
					i--
				}
				if b.tophash[i] != emptyOne {
					break
				}
			}
		notLast:
			h.count--
			break search
		}
	}

	// 第六部分: 解除写保护
	if h.flags&hashWriting == 0 {
		throw("concurrent map writes")
	}
	h.flags &^= hashWriting
}

需要注意的是:

  • 删除key仅仅只是将其对应的tohash值置空,如果kv存储的是指针,那么会清理指针指向的内存,否则不会真正回收内存,内存占用并不会减少
  • 如果正在扩容,并且操作的bucket没有搬迁完,那么会搬迁bucket

4.map中查询元素

  • mapaccess1函数

// map中查找元素
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
	if raceenabled && h != nil {
		callerpc := getcallerpc()
		pc := funcPC(mapaccess1)
		racereadpc(unsafe.Pointer(h), callerpc, pc)
		raceReadObjectPC(t.key, key, callerpc, pc)
	}
	if msanenabled && h != nil {
		msanread(key, t.key.size)
	}
	if h == nil || h.count == 0 {
		if t.hashMightPanic() {
			t.hasher(key, 0) // see issue 23734
		}
		return unsafe.Pointer(&zeroVal[0])
	}
	if h.flags&hashWriting != 0 {
		throw("concurrent map read and map write")
	}
	
	// 第一部分:计算hash值并根据hash值找到桶
	hash := t.hasher(key, uintptr(h.hash0))
	m := bucketMask(h.B)
	b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
	if c := h.oldbuckets; c != nil {
		if !h.sameSizeGrow() {
			// There used to be half as many buckets; mask down one more power of two.
			m >>= 1
		}
		oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
		if !evacuated(oldb) {
			b = oldb
		}
	}
	top := tophash(hash)
	
	// 第二部分:遍历桶和桶串联的溢出桶,寻找key
bucketloop:
	for ; b != nil; b = b.overflow(t) {
		for i := uintptr(0); i < bucketCnt; i++ {
			if b.tophash[i] != top {
				if b.tophash[i] == emptyRest {
					break bucketloop
				}
				continue
			}
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
			if t.indirectkey() {
				k = *((*unsafe.Pointer)(k))
			}
			if t.key.equal(key, k) {
				e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
				if t.indirectelem() {
					e = *((*unsafe.Pointer)(e))
				}
				return e
			}
		}
	}
	return unsafe.Pointer(&zeroVal[0])
}

需要注意的地方:

  • 如果根据hash值定位到桶正在进行搬迁,并且这个bucket还没有搬迁到新哈希表中,那么就从老的哈希表中找。
  • 在bucket中进行顺序查找,使用高八位进行快速过滤,高八位相等,再比较key是否相等,找到就返回value。如果当前bucket找不到,就往下找溢出桶,都没有就返回零值。

5.map的扩容与搬迁

  • 通过上述的map赋值和删除流程,我们知道,触发扩容操作的是map的赋值和删除操作
  • 扩容操作的要点其实在于搬迁
// 扩容
func growWork(t *maptype, h *hmap, bucket uintptr) {
	// 搬迁正在使用的旧 bucket
	evacuate(t, h, bucket&h.oldbucketmask())
	// 再搬迁一个 bucket,以加快搬迁进程
	if h.growing() {
		evacuate(t, h, h.nevacuate)
	}
}
// 是否需要扩容
func (h *hmap) growing() bool {
	return h.oldbuckets != nil
}
// 搬迁bucket
func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
	// 定位老的 bucket 地址
	b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
	// 计算容量 结果是 2^B,如 B = 5,结果为32
	newbit := h.noldbuckets()
	// 如果 b 没有被搬迁过
	if !evacuated(b) {
		// 默认是等 size 扩容,前后 bucket 序号不变
		var xy [2]evacDst
		// 使用 x 来进行搬迁
		x := &xy[0]
		x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))
		x.k = add(unsafe.Pointer(x.b), dataOffset)
		x.v = add(x.k, bucketCnt*uintptr(t.keysize))

		// 如果不是等 size 扩容,前后 bucket 序号有变
		if !h.sameSizeGrow() {
			// 使用 y 来进行搬迁
			y := &xy[1]
			// y 代表的 bucket 序号增加了 2^B
			y.b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize)))
			y.k = add(unsafe.Pointer(y.b), dataOffset)
			y.v = add(y.k, bucketCnt*uintptr(t.keysize))
		}
		// 遍历所有的 bucket,包括 overflow buckets b 是老的 bucket 地址
		for ; b != nil; b = b.overflow(t) {
			k := add(unsafe.Pointer(b), dataOffset)
			v := add(k, bucketCnt*uintptr(t.keysize))
			// 遍历 bucket 中的所有 cell
			for i := 0; i < bucketCnt; i, k, v = i+1, add(k, uintptr(t.keysize)), add(v, uintptr(t.valuesize)) {
				// 当前 cell 的 top hash 值
				top := b.tophash[i]
				// 如果 cell 为空,即没有 key
				if top == empty {
					// 那就标志它被"搬迁"过
					b.tophash[i] = evacuatedEmpty
					continue
				}
				// 正常不会出现这种情况
				// 未被搬迁的 cell 只可能是 empty 或是
				// 正常的 top hash(大于 minTopHash)
				if top < minTopHash {
					throw("bad map state")
				}
				// 如果 key 是指针,则解引用
				k2 := k
				if t.indirectkey {
					k2 = *((*unsafe.Pointer)(k2))
				}
				var useY uint8
				// 如果不是等量扩容
				if !h.sameSizeGrow() {
					// 计算 hash 值,和 key 第一次写入时一样
					hash := t.key.alg.hash(k2, uintptr(h.hash0))
					// 如果有协程正在遍历 map 如果出现 相同的 key 值,算出来的 hash 值不同
					if h.flags&iterator != 0 && !t.reflexivekey && !t.key.alg.equal(k2, k2) {
						// useY =1 使用位置Y
						useY = top & 1
						top = tophash(hash)
					} else {
						// 第 B 位置 不是 0
						if hash&newbit != 0 {
							//使用位置Y
							useY = 1
						}
					}
				}

				if evacuatedX+1 != evacuatedY {
					throw("bad evacuatedN")
				}
				//决定key是裂变到 X 还是 Y
				b.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY
				dst := &xy[useY]                 // evacuation destination
				// 如果 xi 等于 8,说明要溢出了
				if dst.i == bucketCnt {
					// 新建一个 bucket
					dst.b = h.newoverflow(t, dst.b)
					// xi 从 0 开始计数
					dst.i = 0
					//key移动的位置
					dst.k = add(unsafe.Pointer(dst.b), dataOffset)
					//value 移动的位置
					dst.v = add(dst.k, bucketCnt*uintptr(t.keysize))
				}
				// 设置 top hash 值
				dst.b.tophash[dst.i&(bucketCnt-1)] = top // mask dst.i as an optimization, to avoid a bounds check
				// key 是指针
				if t.indirectkey {
					// 将原 key(是指针)复制到新位置
					*(*unsafe.Pointer)(dst.k) = k2 // copy pointer
				} else {
					// 将原 key(是值)复制到新位置
					typedmemmove(t.key, dst.k, k) // copy value
				}
				//value同上
				if t.indirectvalue {
					*(*unsafe.Pointer)(dst.v) = *(*unsafe.Pointer)(v)
				} else {
					typedmemmove(t.elem, dst.v, v)
				}
				// 定位到下一个 cell
				dst.i++
				dst.k = add(dst.k, uintptr(t.keysize))
				dst.v = add(dst.v, uintptr(t.valuesize))
			}
		}
		// Unlink the overflow buckets & clear key/value to help GC.

		// bucket搬迁完毕 如果没有协程在使用老的 buckets,就把老 buckets 清除掉,帮助gc
		if h.flags&oldIterator == 0 && t.bucket.kind&kindNoPointers == 0 {
			b := add(h.oldbuckets, oldbucket*uintptr(t.bucketsize))
			ptr := add(b, dataOffset)
			n := uintptr(t.bucketsize) - dataOffset
			memclrHasPointers(ptr, n)
		}
	}
	// 更新搬迁进度
	if oldbucket == h.nevacuate {
		advanceEvacuationMark(h, t, newbit)
	}
}

五.FQA

1.为什么map遍历是无序的?

  • 因为map底层的扩容与搬迁
  • map在扩容后,会发生key的搬迁,原来在同一个桶的key,搬迁后,有可能就不处于同一个桶了,而遍历map的过程,就是遍历这些桶,桶里的元素发生了变化,那么map遍历当然就是无序的啦

2.map并发访问安全吗?

  • 不安全
  • 有两个解决方法:
    • 加锁
    • 使用golang自带的sync.map

3.map元素为何无法取地址?

  • 因为扩容后map元素的地址会发生变化,归根结底还是map底层的扩容与搬迁

六.小结

  • Golang中,通过哈希表实现map,用拉链法解决哈希冲突
  • 通过将key的哈希值散落到不同桶中,每个桶中8个cell,哈希值的低8位决定在哪个桶,哈希值的高八位决定在桶的的哪个位置
  • 扩容分为等量扩容和2倍增量扩容
  • 当向桶中添加了很多key,造成溢出桶太多,会触发等量扩容,扩容后,原来一个桶中的key会一分为二,重新分配到两个桶中
  • 扩容过程是渐进式的,主要是防止一次扩容要搬迁的元素太多引发性能问题
  • 触发扩容的时间是在新增元素,搬迁的时间是赋值和删除操作期间,每次最多搬迁两个bucket
  • 查找,赋值,删除这些操作一个很核心的内容都是如何定位key的位置

七.参考文章

posted @ 2022-01-18 18:41  西*风  阅读(1613)  评论(0编辑  收藏  举报