map
map的底层实现
- golang中的map采用了HashTable的实现,通过数组+链表实现的。一个哈希表会有一定数量的桶,哈希表将键值对均匀存储到这些桶中。哈希表在存储键值对时,会先用哈希函数把键值转换为哈希值,哈希表先用哈希值的低几位去定位到一个哈希桶,然后再去这个哈希桶中查找这个键。由于键值对总是被捆绑在一起存在,一旦找到了键,就找到了值。go的字典中,每一个键值对都是它的哈希值代表的,字典不会独立存储任何键的键值,但会独立存储他们的哈希值
map的键类型不能是哪些类型
- 不能是函数类型、字典类型、切片类型。
原因:go语言规范规定,键类型的值之间必须可以施加==操作符和!=操作符,即必须支持判等操作,由于函数类型,字典类型,切片类型不支持判等操作
- 如果键类型是接口类型,那么键值的实际类型也不能是上述3种,否则会已发panic
- 如果键的类型是数组类型,那么还要确保该类型的元素类型不是函数类型、字典类型或切片类型。
- 如果键的类型是结构体类型,那么还要保证其中字段的类型的合法性。无论不合法的类型被埋藏得有多深,比如map[[1][2][3][]string]int,Go 语言编译器都会把它揪出来。
map对键类型有何要求
- 求哈希和判等操作比较快的对应的类型适合做键类型。
不建议使用高级数据类型作为类型的原因不仅仅是值求哈希以及判等速度比较慢,而且值存在变数
在值为nil的字典上进行读写操作时会发生什么?
- 除了添加键值对,我们在一个值为nil的字典上做任何操作都不会引起错误,当我们试图在一个值为nil的字典中添加键值对时,会抛出一个panic
map是并发安全的吗
- map类型的值不是并发安全的,即使只是添加或删除操作,也是不安全的,根本原因在于字典值内部有时候会根据需要进行存储方面的调整
sync.map
通过以上几个方面,我们了解了map底层实现,并且知道map不是并发安全的,那么golang有没有提供并发安全的map呢?答案是yes,那就是sync.map。Go官方在2017年发布的Go1.9中,正式加入了并发安全的字典类型sync.map,该map有以下几个特点:
- 并发安全,且虽然用到了锁,但是显著减少了锁的争用。
sync.map出现之前,如果想要实现并发安全的map,只能自行构建,使用sync.Mutex或sync.RWMutex,再加上原生的map就可以轻松做到,sync.map也用到了锁,但是在尽可能的避免使用锁,因为使用锁意味着要把一些并行化的东西串行化,会降低程序性能,因此能用原子操作就不要用锁,但是原子操作局限性比较大,只能对一些基本的类型提供支持,在sync.map中将两者做了比较完美的结合。 - 存取删操作的算法复杂度与map一样,都是O(1)
- 不会做类型检查。
sync.map只是go语言标准库中的一员,而不是语言层面的东西,也正是因为这一点,go语言的编译器不会对其中的键和值进行特殊的类型检查
下面我们通过分析sync.map中的源码,来看上面第1点和第2点是如何做到的
结构体
结构体包含Map、readOnly、entry。
其中Map中包含了两个map:
(1)一个优先读map:read,read不是只允许只读,它是可以有写操作的,但是只允许对已存在的值进行写操作,不允许增加新元素,删除也只是做标记,并不是真正的删除。即键的集合不能被改变,所以键值是不全的
(2)一个需要加锁进行操作的读写map:dirty,其中的键值对集合总是完全的,而且不包含已被逻辑删除的键值对
这两个map是实现并发安全的关键所在
type Map struct{
m Mutex //互斥锁,用于对dirty进行加锁
read atomic.Value //优先读map,支持原子操作,并不是只读map,可以有写操作
dirty map[interface{}]*entry //当前最新map,需要加锁操作,允许读写,
misses int //记录read读取不到数据,需要加锁读取的次数,当misses等于dirty的长度时,会将dirty复制到read
}
readOnly是存储在read中的元素
type readOnly struct{
m map[interface{}]*entry// 该map中存储的entry指针,与dirty中存储的entry指针一样,因此虽然引入的两个map,但是底层存的是指针
amended bool //amended为false时,代表dirty中还没有数据,如果dirty中存在一些在read中没有的数据,则该值为true,可以看作修改的标记
}
entry存放着真正的实体数据
type entry struct{
p unsafe.Pointer
//其中p有如下