Map的底层实现原理
Go语言采用的是哈希查找表,并且使用链表法解决哈希冲突。
另外在面试里面,一般问到Map都会问它是否是并发安全的。
想要回答这类问题,只需要记住这一句话:Go语言提供的数据类型中,只有channel是并发安全的。
Map自身并发安全失败场景
func main() {
//创建map
m := make(map[int]string)
var wg sync.waitGroup
wg.Add(2)
//协程1写
go func() {
for true {
m[0] = "xiaoming"
}
wg.Done()
}()
//协程2读
go func()
for true {
_ = m[0]
}
wg.Done()
}()
wg.wait()
fmt . Print1n("结束")
}
编译后运行会报错:fatal error: concurrent map read and map write
原因
笔记来源:详解Go语言如何解决map并发安全问题
实现并发安全的Map
读写锁+map
只需要在map进行读操作的时候,加读取锁后再进行读取;添加、更新、删除、遍历、获取长度这些操作加写锁再进行操作。
每次进行这些map相关的操作都要进行加锁,我们通过学习Go语言的锁就会知道,读锁可以实现并发读,但是写锁只能单个写,阻塞其他对于此处的读写操作,是独占的。因此这样虽然能够保证数据的安全性,但也会影响程序的运行效率。
所以这种加读写锁的并发安全做法一般适用于读多写少的场景。
分片+锁
分片+锁,顾名思义就是将map分成多个片段,每个片段都由一个锁进行保护。
数据通过散列函数将键分布到不同的小map片段中,从而减少锁的竞争,提高并发性能。
这种方法适用于写操作较多,且写操作分布均匀的场景。分片map的原理是将map划分为多个片段,每个片段由一个单独的锁保护。当对map进行读写操作时,只需要锁定对应的map片段即可。这样可以大大提高并发读写的性能。
sync.Map
Go语言标准库提供的
sync.Map
类型,它不是为了替代普通的map,而是用于特定的场景。sync.Map
适用于读多写少的场景,并且写操作的key
分布比较分散。sync.Map
内部使用了一个只读的map和一个可写的map,读操作默认是无锁的,而写操作需要加锁。当
misses
(未命中读map的次数)超过一定阈值时,会将可写的map复制到只读map中。sync.Map
不能被复制,且在高频率写操作时性能可能下降。sync.Map
是 Go 语言中提供的一个并发安全的 map 类型,它允许多个 goroutine 安全地使用 map 进行读写操作而不需要使用互斥锁(mutex)。sync.Map
包里有增删改查等 Go 官方封装好的方法,用起来很方便嗷!具体的话还请看我之前写的:[[Golang基础] Sync包](https://rolingg.top/index.php/archives/103/)
读写锁+map 和 Sync.Map 相比较:
读写锁+map
优点:
- 简单直观:使用
sync.RWMutex
来保护一个普通的map,读操作使用读锁,写操作使用写锁。 - 适合读多写少的场景:允许多个读操作并发执行,而写操作是独占的,适合读操作远多于写操作的情况。
缺点:
- 读操作性能受限:在高并发读操作下,虽然可以并发读取,但如果写操作频繁,读操作会因为锁的争用而受到性能影响。
- 写操作独占:写操作需要独占锁,这会阻塞所有的读和写操作,可能导致性能瓶颈。
sync.Map
优点:
- 并发性能高:
sync.Map
通过读写分离和延迟删除机制,优化了读多写少的场景下的并发性能。 - 空间换时间:使用两个map(read和dirty)来提高性能,减少了锁的使用,提高了并发读取的效率。
- 延迟删除:删除操作不会立即清理内存,而是标记为删除,直到下次同步时才清理,减少了锁的操作。
缺点:
- 不适合大量写入的场景:在写操作频繁的场景下,
sync.Map
的性能可能下降,因为需要频繁地将dirty中的数据同步到read中。 - 内存使用高:由于维护了两个map,可能会增加内存的使用。
综上,sync.Map
对读操作加锁的优化更好,但是对于一些需要复制map,且对内存占用要求小的场景,用简单实现的读写锁+map会更好。
总结
写场景频繁,对性能要求也比较高的话,还是使用 分片+锁
的方式吧;其余的场景得看对内存的要求,对内存占用要求小的话,就用 读写锁+map
,没这个需求的话,就用 sync.Map
,方便快捷。
评论(0)