golang中的map与sync.map解析

本文详细解析了Go语言中的map和sync.Map。map底层采用哈希表实现,键类型需要支持判等操作,且并发不安全。sync.Map是Go1.9引入的并发安全字典,它在并发环境下提供了更好的性能,但不进行类型检查。文章介绍了sync.Map的结构和主要方法,包括Load、Store、LoadOrStore、Delete、Range,以及两种保证键类型和值类型正确性的方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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有以下几个特点:

  1. 并发安全,且虽然用到了锁,但是显著减少了锁的争用
    sync.map出现之前,如果想要实现并发安全的map,只能自行构建,使用sync.Mutex或sync.RWMutex,再加上原生的map就可以轻松做到,sync.map也用到了锁,但是在尽可能的避免使用锁,因为使用锁意味着要把一些并行化的东西串行化,会降低程序性能,因此能用原子操作就不要用锁,但是原子操作局限性比较大,只能对一些基本的类型提供支持,在sync.map中将两者做了比较完美的结合。
  2. 存取删操作的算法复杂度与map一样,都是O(1)
  3. 不会做类型检查。
    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有如下取值:
//(1)如果p==nil,read中元素已经被删除, m.dirty == nil
//(2)如果p==expunged:表示read中的元素已经被删除,这种情况出现在将read复制到dirty中,即复制的过程会先将nil标
sync.Map 是 Go 语言标准库中提供的一种并发安全的字典类型,它可以被多个 goroutine 安全地访问修改。在多个 goroutine 中并发地读写一个 map 时,会出现竞争条件,从而导致数据不一致。而 sync.Map 利用了一些锁的技巧,避免了这种竞争条件的发生,从而实现了高效的并发安全访问。 sync.Map 的 API 非常简单,主要包括以下几个方法: 1. Store(key, value interface{}):将一个键值对存储到 sync.Map 中。 2. Load(key interface{}) (value interface{}, ok bool):根据键从 sync.Map 中获取对应的值。 3. LoadOrStore(key, value interface{}) (actual interface{}, loaded bool):如果键存在于 sync.Map 中,则返回对应的值 true,否则将键值对存储到 sync.Map 中并返回新的值 false。 4. Delete(key interface{}):从 sync.Map 中删除一个键值对。 5. Range(f func(key, value interface{}) bool):遍历 sync.Map 中的键值对,并对每一个键值对调用函数 f,如果 f 返回 false,则停止遍历。 下面是一个使用 sync.Map 的简单例子,展示了如何在多个 goroutine 中并发地访问修改 sync.Map: ``` package main import ( "fmt" "sync" ) func main() { var m sync.Map var wg sync.WaitGroup wg.Add(2) // goroutine 1: 向 sync.Map 中存储键值对 go func() { defer wg.Done() m.Store("key1", "value1") m.Store("key2", "value2") }() // goroutine 2: 从 sync.Map 中加载键值对 go func() { defer wg.Done() if v, ok := m.Load("key1"); ok { fmt.Println("value for key1:", v) } if v, ok := m.Load("key2"); ok { fmt.Println("value for key2:", v) } }() wg.Wait() } ``` 在上面的例子中,我们首先创建了一个 sync.Map 对象 m。然后在两个 goroutine 中同时访问这个对象,一个 goroutine 向其中存储键值对,另一个 goroutine 则从其中加载键值对。由于 sync.Map 是并发安全的,所以这两个 goroutine 可以并发地访问修改 sync.Map,而不会出现竞争条件。 需要注意的是,虽然 sync.Map 是并发安全的,但它并不是用来替代普通的 map 的。如果你只是需要在某个 goroutine 中访问修改一个 map,那么你应该使用普通的 map,因为 sync.Map 的性能会比较差。只有在需要多个 goroutine 并发地访问修改一个 map 时,才应该考虑使用 sync.Map
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值