面试官:你好,我们来聊下 Go 语言的 map 。首先,请聊下 Go 语言的 map 是不是并发安全的?
应试者:不是的。Go 语言的 map 不是并发安全的。如果在多个 goroutine 同时读写同一个 map 时,会出现竞态条件( race condition ),可能导致程序运行出错,甚至崩溃。
为了证明这一点,我可以展示一个并发不安全的示例:
package main import ( "fmt" "sync" ) func main() { m := make(map[int]int) var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func(n int) { defer wg.Done() m[n] = n }(i) } wg.Wait() fmt.Println("Map 的大小:", len(m)) }
这段代码可能会出现以下问题:
面试官:OK 。那当我们需要在并发场景下使用 map 时,你有什么好的解决方案吗?
应试者:在 Go 语言中,主要有三种解决方案:
sync.Mutex
互斥锁来保护 maptype SafeMap struct { sync.Mutex m map[int]int } func (sm *SafeMap) Set(key, value int) { sm.Lock() defer sm.Unlock() sm.m[key] = value } func (sm *SafeMap) Get(key int) (int, bool) { sm.Lock() defer sm.Unlock() val, exists := sm.m[key] return val, exists }
sync.RWMutex
,允许并发读,但写入互斥type SafeMap struct { sync.RWMutex m map[int]int } func (sm *SafeMap) Get(key int) (int, bool) { sm.RLock() defer sm.RUnlock() val, exists := sm.m[key] return val, exists }
sync.Map
,这是 Go 标准库提供的并发安全的 map 实现var m sync.Map // 存储 m.Store("key", "value") // 读取 value, ok := m.Load("key") // 删除 m.Delete("key")
面试官:能详细解释一下为什么普通的 map 不是并发安全的吗?这背后的机制是什么?
应试者:这涉及到 Go 语言 map 的底层实现。在 Go 的源码中( runtime/map.go ),map 的结构大致是这样的:
type hmap struct { count int // 元素个数 flags uint8 // 状态标志 B uint8 // 桶的对数 noverflow uint16 // 溢出桶的近似数 hash0 uint32 // 哈希种子 buckets unsafe.Pointer // 桶数组 oldbuckets unsafe.Pointer // 旧桶数组,在扩容时使用 // ... 其他字段 }
并发不安全的根本原因在于:
具体来说,一个简单的写入操作可能包含多个步骤:
这些步骤如果被并发执行,就会导致不可预期的结果。
面试官:sync.Map
是如何解决这些并发问题的?能详细介绍一下它的实现原理吗?
应试者:sync.Map
的核心设计是读写分离和优化的并发控制。我们可以看一下它的大致结构:
type Map struct { mu Mutex read atomic.Value // readOnly dirty map[interface{}]*entry misses int } type readOnly struct { m map[interface{}]*entry amended bool // 是否有新的数据在 dirty 中 }
它的主要优化策略包括:
双层存储:
read
map:无锁读取dirty
map:需要加锁的可写 map读优化:
read
map 读取atomic.Value
保证读取的线程安全写入机制:
read
map 中更新dirty
map动态提升:
dirty
map 被频繁访问时,会将其提升为 read
map实际的读写流程大致如下:
func (m *Map) Load(key interface{}) (value interface{}, ok bool) { // 首先无锁读取 read map read, _ := m.read.Load().(readOnly) e, ok := read.m[key] if !ok && read.amended { // 如果 read map 没有,且有新数据,则加锁查询 dirty map m.mu.Lock() // 双检查,避免重复加锁 read, _ = m.read.Load().(readOnly) e, ok = read.m[key] if !ok && read.amended { e, ok = m.dirty[key] // 记录未命中次数,可能会晋升 dirty map m.missLocked() } m.mu.Unlock() } // ... 返回结果 }
面试官:那么,sync.Map
是不是在所有并发场景下都是最佳选择?
应试者:不是的。sync.Map
有其特定的适用场景和局限性:
适用场景:
不适用场景:
性能建议:
sync.Mutex + map
可能更高效面试官:最后,你能分享一个实际工作中处理并发 map 的最佳实践吗?
应试者:在高并发缓存场景,我曾使用分片锁方案来优化 map 的并发性能:
type ShardedMap struct { shards []map[string]interface{} locks []sync.RWMutex shardCount int } func NewShardedMap(shardCount int) *ShardedMap { sm := &ShardedMap{ shards: make([]map[string]interface{}, shardCount), locks: make([]sync.RWMutex, shardCount), shardCount: shardCount, } for i := 0; i < shardCount; i++ { sm.shards[i] = make(map[string]interface{}) } return sm } func (sm *ShardedMap) getShard(key string) (map[string]interface{}, *sync.RWMutex) { hash := fnv32(key) shardIndex := hash % uint32(sm.shardCount) return sm.shards[shardIndex], &sm.locks[shardIndex] } func (sm *ShardedMap) Set(key string, value interface{}) { shard, lock := sm.getShard(key) lock.Lock() defer lock.Unlock() shard[key] = value } func fnv32(key string) uint32 { hash := uint32(2166136261) for i := 0; i < len(key); i++ { hash *= 16777619 hash ^= uint32(key[i]) } return hash }
这种方案的优点:
面试官:很好,你对 map 的并发问题理解的相当充分。
应试者:非常感谢!
更多 Go 高质量内容: https://portal.yunosphere.com
欢迎关注我,经常分享有用的 Go 知识 / 面试 / 创业经历 / 其他编程知识
1 wolfsun 323 天前 ![]() 很佩服楼主能有这样的行动力做一件事,在这个时代就是要这样才能搞到钱,也是挺难的。 |
![]() | 2 tcpdump 323 天前 笑死,除了面试,很少会专研这个,你写业务的时候,写测试,不就测出来了吗?面试钻某个点的都是装的 |
3 yingxiangyu 323 天前 @tcpdump #2 但是实际上问的这些问题本质都是对哈希表的优化,就算不是面试,有多线程大量读写共享最终也是这个方案,跟语言、面试也没啥关系 |
![]() | 4 tcpdump 323 天前 @yingxiangyu 对啊,就是内存共享、争用、锁、一致性啊,业务都要考虑的,不要关注代码层,应该是先有思路再去专研,实现业务需求 |
5 DefoliationM 323 天前 via Android 好的,知道了,回去等通知吧。 |
![]() | 7 YunFun OP @yingxiangyu 对的哈哈 |
![]() | 8 YunFun OP @DefoliationM 经典等通知 |
![]() | 9 OP @wolfsun 感谢感谢,喜欢研究技术,也做点自己喜欢的事,能有收入就更好了 |
![]() | 10 grzhan 323 天前 fastcache 也是分片锁的思路,切成 512 个 buckets ,进一步最主要就是针对 GC 做了优化,索引用 map[int]int noscan 来减小 GC 扫描开销,实际 key,value 放在一个自己手动 mmap 分配管理的 chunks ([][]bytes )里,跳过了 golang 自己的堆 gc 。这套思路上生产很多场景应该是够用了 |
11 CLMan 323 天前 或许没有一个编程语言能逃过八股文。 |
12 kingcanfish 323 天前 什么时候卖课 |
![]() | 14 CEBBCAT 323 天前 感觉写得还不错,前两天另外一个人发 goroutine 还是什么的,被我评论区“省流”了。 虽然写得不错,但我读完感觉更期望有若干篇从软件设计角度研讨的文章楼上也有人提到,这些代码背后其实就是运用不同策略应对不同场景的需求那样的话想必能够提纲挈领,促使读者举一反三 |
15 bitfly 323 天前 via Android 这个我用过 在读取一段 ip 列表时 比如有 100 万个 ip 需要验证是否能 ping 通 但是不能顺序读取 需要随机从里面的列表读取 且不能漏 且需要并发每次读取 100 个不然一个个读取要几个月时间 且有超时条件 就需要用这个方法 但是也偶尔会触发 panic 如果不 panic 那处理的效率是 c#的数倍 |
![]() | 17 COW 323 天前 via Android 八股文太多了,如果 op 能结合实际场景去分享,我觉得更吸引人一点。 |
18 ryan961 323 天前 面试官:不错,基础很扎实。能再谈一谈你对 Go 1.24 即将落地的 swiss table 的看法吗? |
![]() | 19 6HWcp545hm0RHi6G 323 天前 @ryalu 哈哈哈哈 接着问 |
![]() | 21 Felldeadbird 323 天前 感谢楼主分享。我写 go 才小半年,还是菜鸟一个,问我 map 的我都答不上。 |
![]() | 22 gongxuanzhang 323 天前 在 Java 八股面前,这玩意和小儿科一样 |
![]() | 23 zbatman 323 天前 面试官:好,那喜欢爸爸还是妈妈? |
![]() | 26 YunFun OP @kingcanfish 在卖了在卖了 |
![]() | 27 YunFun OP @cydian 听劝。改版下官网,最近出个公开课模块哈哈,其实前段时间搞了点免费课程 https://yunosphere.com ,最近也在一边更新一边重新规划公开的内容~ 欢迎关注 ,感谢宝贵的建议 |
![]() | 33 YunFun OP @gongxuanzhang #22 哈哈哈,jvm 原理都是入门对吧 |
![]() | 34 YunFun OP @Felldeadbird #21 能帮到你最好哈哈,互相学习 欢迎关注 |
![]() | 36 gongxuanzhang 323 天前 @YunFun 这点锁的知识连培训班都得教了 |
![]() | 37 YunFun OP @gongxuanzhang #36 哥们,本来我是做好被喷卖课的准备的,结果倒是被喷不如培训班 首先我是卖课需要积累自己的内容所以在摸索着做,想解决的是想学学其他技能这部分人的需求顺便看能否赚点钱,不做培训班,也没那能力偶 其次培训班面向的是包就业,这不是我的目的。真的工作经验积累也不是培训班那点套路东西能搞定的不是 最后培训班也是要收费的,我花时间写了,至少这里还是免费的 |
![]() | 38 gongxuanzhang 322 天前 @YunFun 首先对伤害到你感到抱歉,并没有想嘲讽你和针对你。 我想表达的就是关于 Map 的线程安全问题和如何解决衍生的一系列知识。 我觉得对于 Java 面试来说这个就是非常初级的八股,哪怕和 JVM 相关也是初级八股。 这个初级不是说知识有多简单,而是因为它被问的频繁,我不太了解 go ,仅仅是在语法层面而已。 祝你大卖 |