map 并不是并发安全的
map是引用类型,如果两个map同时指向一个底层,那么一个map的变动会影响到另一个map。 map 的并发不安全主要是因为缺乏内置的锁机制和原子操作,并且在扩容过程中可能会导致数据不一致。
无锁机制: map 的底层数据结构是一个哈希表,当多个 Goroutine 并发地对 map 进行读写操作时,可能会导致数据竞争(DataRace)。因为 map 没有内置的锁机制来保护并发访问,所以在并发读写时可能会造成数据不一致或损坏。
非原子操作: 尽管 map 的操作看起来是单个的语句,比如 m[key] = value 和 delete(m,key),但实际上它们都不是原子操作。在并发环境中,一个 map 操作可能会由多个机器指令组成,而且在执行期间可能会被中断或切换到另一个Goroutine,这就增加了出错的可能性。
扩容操作【重点】: 当 map 的元素个数超过了当前容量的时候,Go 语言会自动扩容map,这时会重新分配内存,并将原有的键值对重新哈希到新的内存位置。在扩容过程中,如果有其他 Goroutine并发地进行读写操作,可能会导致指针失效、丢失数据或者死锁等问题。
因此,这意味着在多个 Goroutine 同时读取和写入 map 时可能会导致竞态条件(Race Condition)和数据竞争(Data Race)。当多个 Goroutine 并发地读取和写入同一个 map 时,可能会出现不可预期的结果,甚至导致程序崩溃。
要保证 map 的并发安全,可以采取以下几种方式:
使用互斥锁(Mutex)
使用读写互斥锁(RWMutex)
使用并发安全的数据结构:sync.Map
1. 使用互斥锁(Mutex)
可以在读取和写入 map 的操作上加锁,保证同一时刻只有一个 Goroutine 能够访问 map。通过 sync.Mutex 提供的锁机制可以很容易地实现这一点。
go
import "sync"
var mu sync.Mutex
var m = make(map[string]int)
// 写操作
mu.Lock()
m["key"] = value
mu.Unlock()
// 读操作
mu.Lock()
value := m["key"]
mu.Unlock()
2. 使用读写互斥锁(RWMutex):
如果多个 Goroutine 需要同时读取 map,但只有一个 Goroutine 写入 map,可以使用 sync.RWMutex 提供的读写锁机制。这种方式可以提高并发读取的效率。
go
import "sync"
var mu sync.RWMutex
var m = make(map[string]int)
// 写操作
mu.Lock()
m["key"] = value
mu.Unlock()
// 读操作
mu.RLock()
value := m["key"]
mu.RUnlock()
3. 使用并发安全的数据结构:
在 Go 语言的 sync 包中还提供了一些并发安全的数据结构,例如 sync.Map。使用 sync.Map 可以避免手动管理锁,简化并发编程。
go
import "sync"
var m sync.Map
// 写操作
m.Store("key", value)
// 读操作
value, ok := m.Load("key")
总的来说,要保证 map 的并发安全,需要在并发访问时进行适当的同步控制,使用锁机制或并发安全的数据结构来保护 map 的读写操作。
===========================
额外知识:
- map是引用类型,如果两个map同时指向一个底层,那么一个map的变动会影响到另一个map。
- map的零值(Zero Value)是nil,对nil map进行任何添加元素的操作都会触发运行时错误(panic)。因此,使用前必须先创建map,使用make函数,例如:m := make(map[string]int)。
- map的键可以是任何可以用 == 或!=操作符比较的类型,如字符串,整数,浮点数,复数,布尔等。但是,slice,map,和function类型不可以作为map的键,因为这些类型不能使用==或!=操作符进行比较。
- map在使用过程中不保证遍历顺序,即:map的遍历结果顺序可能会不一样,所以在需要顺序的场合,要自行处理数据并排序。
- map进行的所有操作,包括读取,写入,删除,都是不安全的。也就是说,如果你在一个goroutine中修改map,同时在另一个goroutine中读取map,可能会触发"concurrent map read and map write"的错误。