Go Map

3.Map

Go中的map使用哈希表作为底层实现,一个哈希表里可以有多个哈希表节点(bucket)

3.1 初始化

var map = make(map[int]string,10)
map := make(map[int]string,10)
使用make进行初始化时,make中最多只能有两个参数,第一个参数为map的类型,第二个参数为map的容量

3.2 map数据结构

runtime/map.go/hmap

结构关键字段

type hmap struct {
	count     int    // 元素的个数
	B         uint8  // buckets 数组的长度就是 2^B 个
	noverflow uint16 //使用的溢出桶数量
    
	buckets    unsafe.Pointer // 2^B个桶对应的数组指针
	oldbuckets unsafe.Pointer  // 发生扩容时,记录扩容前的buckets数组指针
    nevacuate uintptr //增量扩容阶段 即将迁移的旧桶编号

	extra *mapextra //用于保存溢出桶的信息
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hFRHiyVH-1662515413874)(D:\markdown文件\go\go原理_photo\Snipaste_2022-06-10_16-11-20.png)]

上图中hmap.B = 2,hmap.buckets的长度为2^B =4。通过key的hash值的后“B”位确定是哪一个桶。

3.2.1 初始化Map时B的计算方式
//初始化一个key为string类型 value为int类型 容量为16的map
map := make([int]string,16)
  • B的计算方式
负载因子 = count(map中存储元素个数)/2^B (2^B为bucket的数量)
由于设定 负载因子 > 6.5 map扩容
所以 初始化时 负载因子应 <= 6.5
套用第一行公式
6.5 >= count/2^B
2^B * 6.5 >= count
初始化定义的count 为16
即
2^B * 6.5 >= 16
此时开始计算B
B= 0 1*6.5 < 16 不成立
B=1 2*6.5 < 16 不成立
B=2 4*6.5 >= 16 成立

由此得 B=2

3.3 bucket数据结构

runtime/map.go/bmap

type bmap struct {
	tophash [bucketCnt]uint8
}

//在编译期间会产生新的结构体
type bmap struct {
    tophash [8]uint8 //存储哈希值的高8位
    data    byte[1]  //key value数据:key/key/key/.../value/value/value...
    overflow *bmap   //溢出bucket的地址
}

map中确定键值对 存放在哪个桶的方法

1.取模法
	hash值%桶的个数 -> hash%m
2.与运算
	此运算要求 桶的个数是2的整数次幂
	hash值&最大桶下标 -> hash&(m-1)

go中采用的 确定键值对 存放在哪个桶的方法

hash值的后B位
hmap中B = 4 则桶数2^B = 2^4 = 16
拿到hash值得后4位 转为10进制 即为 该键值对要存放的桶的编号

此方法和 与运算 的结果相同
分析
B = 4 桶的总数16 桶的编号范围[0,15] 最大桶号为15 桶号 的格式 一定为 0000 1111 或 0000 0011这种形式 记录最大桶号 的二进制中 1出现的位数 例如15的 二进制数 后4位为1 则查看hash值的后4为即可
假设 该hash值为1010 1010 15的二进制为 0000 1111
与运算 结果为 0000 1010 -> 10 该键值对放10号桶

取hash值后B位
B = 4 hash值后4位 = 1010 结果和与运算一样

在这里插入图片描述

3.4 put

如上图

1.假设该map的B为4,即buckets数组大小为2^4 = 16
2.将某个元素存入map
3.对key进行计算得到hash的值,hash值的后B位(B为4,即后4位,此处假设为0101)十进制结果是5,所以放在buckets数组的下标为5的元素(bmap)中
4.获取hash值的高8位,如果tophash数组中已经存在该值,则更新对应的value,如果不存在,则放入bmap的tophash中,将key放入keys,value放入values中
5.如果当前的桶存满时,会创建一个新的桶,让overflow指向该桶,将新的元素放入该桶中

3.5 get

如3.3的图

1.假设该map的B为4,即buckets数组大小为2^4 = 16
2.根据key获取map中的value
3.对key进行hash计算,拿到后B位(B=4,即后4位,假设为0101),十进制结果为5,即在buckets的下标为5的元素中
4.拿到hash值的高8位,与bucket中tophash数组的各个元素比较
	4.1 如果找到,获取tophash对应的value,返回
	4.2 如果没有找到,去overflow指向的溢出桶中按照如上步骤寻找
	4.3 如果最终没有找到,会返回value类型的默认值
	4.4 如果map处于搬迁过程,则优先从oldbuckets查找

3.6 哈希冲突

指的是:两个或以上的key经过hash计算,后B位相同,要放入同一个bucket中

解决哈希冲突的方法

1.开放地址法
	根据hash值获取到桶号后,如果发现该桶中已经存储键值对,则查看下一个桶中是否可以存放...
2.链地址法
	如下

Go的解决方法

链地址法:
每一个桶中,默认可以存放8个元素,当该桶存满时,会创建一个新桶,让该桶的overflow指向新桶,将新元素放入新桶中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DMlNiqm3-1662515413875)(D:\markdown文件\go\go原理_photo\GO专家编程.jpg)]

3.7 扩容

  • 负载因子:map中每个桶的平均元素个数
loadFactor = count/2^B
负载因子 = map中元素个数/桶的个数
  • 扩容条件
1.当负载因子 > 6.5 时
2.当溢出桶数量过多时
	2.1 当B<15时,如果overflow存储的溢出桶数量超过2^B
	2.2 当B>=15时,如果overflow存储的溢出桶数量超过2^15
3.7.1 扩容方式
  • 等量扩容
并没有扩大map中bucket的数量,而是把松散的键值对重新排列一次,使bucket的使用率更高。
当进行大量的增删操作后,键值对正好集中在一小部分bucket中,会造成overflow指向的bucket数量很多,但是map的负载因子并不高,从而无法指向增量扩容
如图所示:

在这里插入图片描述

把后置的元素整理到前面的bucket中。元素会发生重排,但不会换桶。
  • 增量扩容
当负载因子过大时,就新建一个bucket数组,新的bucket长度是原来的两倍,将旧的bucket中元素迁移到新的bucket中。用hmap中的nevacuate字段记录旧桶迁移的进度。如果map中存储了大量的数据,一次性迁移会造成比较大的延时,Go采用逐步迁移策略,即每次访问map时都会触发一次迁移,每次迁移2个键值对。

例子:

0.map中要放入新元素
1.当前B = 1 桶的个数2^1 = 2 此时两个桶中都存放7个元素
2.负载因子 loadFactor = 14/2 = 7 > 6.5 触发增量扩容
3.让map中的oldbuckets指向旧的桶数组,创建一个新桶数组,容量为旧桶数组的2倍,hmap中B+1, buckets指向新桶数组
4.新元素根据put执行过程放入新桶数组的某个桶中
5.后续对map的访问操作会触发迁移,将oldbuckets中的键值对逐步迁移到buckets中,当oldbuckets中元素全部迁移完成,删除oldbuckets指向的桶数组
5.在迁移没完成前,每次get或put都会先遍历oldbuckets,再遍历buckets

图中只画了一个桶,实际上为2个桶

在这里插入图片描述

插入新元素示意图(扩容应该为4个桶,简化图只画了2个桶)

在这里插入图片描述

搬迁完成后示意图

在这里插入图片描述

注意:

增量扩容后,元素会重排,元素可能会发生桶迁移
1.例如上图中,扩容前map.B = 1,即看key的hash值的后1位 来确定该元素放到哪个bucket中
2.扩容后,map.B = 2 迁移时,key的hash值看后2位
3.扩容前后B的不同,导致同一个元素 在扩容前后放入桶数组的下标 可能不同

3.8 map数据不可取地址

在这里插入图片描述

map[string]Student 的value是一个Student结构体值
当list["studnet"] = student 时,是一个值拷贝过程
而list["student"] 是一个值引用,值引用的特点是 只读
所以 无法修改list["student"]的Name
  • map中元素的Value类型从Student值类型 改为Student指针类型

此时我们修改的是指针指向的Student

package main

import "fmt"

type Student struct {
	Name string
}

var list map[string]*Student

func main() {

	list = make(map[string]*Student, 5)

	student := Student{"Aceld"}

	list["student"] = &student
	list["student"].Name = "LDB"

    fmt.Println(student)//{LDB}
}

3.9 map的遍历赋值

package main

import "fmt"

type student struct {
	Name string
	Age  int
}

func main() {
	m := make(map[string]*student)

	stus := []student{
		{Name: "zhou", Age: 10},
		{Name: "li", Age: 11},
		{Name: "wang", Age: 12},
	}
	for _, stu := range stus {
		m[stu.Name] = &stu
	}
	for k, v := range m {
		fmt.Println(k, "=>", v.Name)
	}
}
zhou => wang
li => wang  
wang => wang

问题

map的value类型为*student 指向student变量的地址
在foreach循环之后,每个key对应的都是stu数组中最后一个元素的地址

正确写法

for i := 0; i < len(stus); i++ {
	m[stus[i].Name] = &stus[i]
}

3.10 map的线程不安全

在同一个时间点,两个goroutine对同一个map进行读写操作是不安全的。

例子:

1.map.B = 2 buckets指向的桶数组大小为4
2.goroutine1要插入一个新元素
3.goroutine2 计算key2的hash值 B=2 确定key2存放于桶1
3.插入新元素,负载因子 > 6.5 此时增量扩容 B = 3 桶变为6个
4.增量扩容导致 buckets元素迁移到oldbuckets
5.goroutine2从桶1中遍历,获取数据失败

解决方法

当对一个map进行并发读写时,采用mutex锁
package main

import (
	"fmt"
	"sync"
	"time"
)

type Resource struct {
	sync.RWMutex
	m map[string]int
}

func main() {
	r := Resource{
		m: make(map[string]int),
	}
	go func() {
		for j := 0; j < 10; j++ {
			r.Lock()
			r.m[fmt.Sprintf("resource_%d", j)] = j
			r.Unlock()
		}
	}()
	go func() {
		for j := 0; j < 10; j++ {
			r.RLock()
			fmt.Println(r.m[fmt.Sprintf("resouce_%d", j)])
			r.RUnlock()
		}
	}()
	time.Sleep(time.Second)
}

3.11 mapextra结构体

type mapextra struct {
	overflow    *[]*bmap //已经使用的溢出桶地址
	oldoverflow *[]*bmap // 存储 扩容阶段 旧桶用到的那些溢出桶的地址
	nextOverflow *bmap //下一个空闲的溢出桶
}

当B>4时,Go中认为使用溢出桶的几率较大,在初始化桶时 会预分配2^(B-4)个溢出桶备用。这些溢出桶 和 常规桶 在内存中是连续的。前2^B个作为常规桶,后面的用作溢出桶。

源码 runtime/map.go 中 makeBucketArray函数

if b >= 4 {
    // Add on the estimated number of overflow buckets
    // required to insert the median number of elements
    // used with this value of b.
    nbuckets += bucketShift(b - 4)
    sz := t.bucket.size * nbuckets
    up := roundupsize(sz)
    if up != sz {
        nbuckets = up / t.bucket.size
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值