概述
Go语言中的map是一种常用的数据结构,它可以用于快速的查找和更新键值对。本文将会介绍map的底层实现、扩容机制原理以及并发读写问题。
底层实现
Go语言中的map底层实现是一个哈希表,它使用了拉链法来处理哈希冲突。哈希表是一个数组,每个数组元素是一个链表,哈希函数将键映射到数组中的索引,键值对就被放置在该索引对应的链表上。
在Go语言中,哈希表结构的定义如下:
type hmap struct {
count int // 元素数量
B uint8 // 2^B = 桶的数量
noverflow uint16 // 溢出桶数量
hash0 uint32 // 哈希种子
buckets unsafe.Pointer // 桶数组指针
...
}
其中,B表示桶的数量等于2^B,noverflow表示溢出桶的数量,buckets是指向桶数组的指针,定义如下:
type bmap struct {
tophash [bucketCnt]uint8 // 保存哈希值小于8的键的哈希值
data [bucketCnt]keyValuePair // 键值对
overflow *bmap // 溢出桶指针,用于解决哈希冲突
...
}
每个桶包含了一个保存键值对的数组和一个指向溢出桶的指针。如果两个键映射到同一个桶中,就将其存储在溢出桶中。
扩容机制
哈希表的扩容机制如下:当元素数量 count 大于桶的数量 2^B 时,扩容并将桶的数量翻倍。为了避免复制整个数据结构,扩容时会创建一个新的哈希表,然后逐个复制旧哈希表中的元素到新哈希表,并同时更新旧哈希表的指向新哈希表的指针。
具体的扩容过程如下:
- 申请一个新的哈希表的存储空间,大小为$2*2^{B+1}$;
- 将桶的数量
B加 1; - 计算所有元素的新哈希值,将其存储到新哈希表的对应桶中;
- 如果溢出桶不为空,则将其中的元素逐个复制到新哈希表的对应桶中;
- 将新哈希表指向旧哈希表的指针替换为旧哈希表的指向新哈希表的指针;
- 将旧哈希表中的桶的数量
B更新为新的桶的数量; - 将溢出桶的数量
noverflow重置为 0; - 释放旧哈希表的空间。
并发读写问题
由于map底层使用了哈希表,因此并发读写问题也存在。在多个goroutine同时修改map,在扩容时也存在潜在的问题。为了解决这些问题,Go语言采用了读写互斥锁机制来保证并发安全。
读写锁(sync.RWMutex)会在使用map时自动添加读锁,但对于写操作,需要使用显式的写锁定(sync.Mutex),以确保在集合被更改期间,不允许其他goroutine修改它。
以下是并发读写map中的示例代码:
// 定义一个 map,用于保存计数器
var clicks = make(map[string]int)
// 定义读写锁
var rw sync.RWMutex
// 定义一个 goroutine,用于累加计数器
func updateClicks(id string) {
// 获取写锁
rw.Lock()
// 操作计数器
clicks[id]++
// 释放写锁
rw.Unlock()
}
// 定义另一个 goroutine,用于读取计数器
func getTotalClicks() int {
// 获取读锁
rw.RLock()
// 操作计数器
total := 0
for _, count := range clicks {
total += count
}
// 释放读锁
rw.RUnlock()
return total
}
在上述代码中,调用updateClicks()函数时,首先获取写锁,然后对计数器进行操作。类似地,在调用getTotalClicks()函数时,首先获取读锁,然后对计数器进行操作。
通过使用读写锁,可以确保在写时独占map,并在读取时允许多个goroutine同时读取map,从而提高程序的并发性能。
总结
map是Go语言中使用频率极高的数据结构之一,它的底层实现采用了哈希表的方式来处理键值对,采用了扩容机制来确保在map中添加足够的元素时,不会降低程序性能,并通过读写锁解决了并发读写的问题。在使用map时,需要注意使用读写锁来保证并发安全。
文档信息
- 本文作者:KcJia
- 本文链接:https://blog.kcjia.cn/2023/03/30/go-map/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)