1. map 使用注意的点,是否是并发安全的?#
分析
考察map的线程安全,map在使用过程中主要是要注意并发读写不加锁会造成fatal error,让程序崩溃。并且这种错误是不能被recover捕获的
回答
map 不是线程安全的。
如果某个任务正在对map进行写操作,那么其他任务就不能对该 字典执行并发操作(读、写、删除),否则会导致进程崩溃。
在查找、赋值、遍历、删除的过程中都会检测写标志,一旦发现写标志等于1,则直接 fatal 退出程序。赋值和删除函数在检测完写标志是0之后,先将写标志改成1,才会进行之后的操作。
2. map 循环是有序的还是无序的?#
分析
考察对map遍历的底层实现是否了解,map在每次遍历的时候都会选定一个随机桶号还有槽位,遍历从这个随机桶开始往后依次便利完所有的桶,在每个桶内,则是按照之前选定随机槽位开始遍历,回答的时候要突出随机桶号和槽位
回答
map的遍历是无序的,map每次遍历,都会从一个随机值序号的桶,在每个桶中,再从按照之前选定随机槽位开始遍历,所以是无序的。
补充问题:为什么go语言的map要这样设计,要随机选定桶号和槽位进行随机遍历?
分析
因为map是可以动态扩容的,map 在扩容后,会发生 key 的搬迁,这样 key 的位置就会发生改变,那么如果顺序遍历key,在扩容前后顺序肯定会不一样,这道题回答一定要突出扩容会带来key的位置发生变化
回顾一下双倍扩容,key的变迁过程,双倍扩容,目标桶容后的位置可能在原位置也可能在原位置+偏移量处。

回答
因为map 在扩容后,会发生 key 的搬迁,原来落在同一个 bucket 中的 key,搬迁后,有些 key 的位置就会发生改变。而遍历的过程,就是按顺序遍历 bucket,同时按顺序遍历 bucket 中的 key。搬迁后,key 的位置发生了重大的变化,这样,遍历 map 的结果就不可能按原来的顺序了。所以,go语言,强制每次遍历都随机开始。
3. map 如何顺序读取?#
分析
这个题目实际上是上一个题目的补充,因为map本身的遍历是不能顺序执行的,所以我们要达到一个顺序遍历的目的就不能用原map的遍历方式,要想顺序遍历,显然需要对map的key进行排序,然后,我们按照这个排序之后的key从map里面取出对应的数据即可
代码示例:
package main
import (
"fmt"
"sort"
)
func main() {
keyList := make([]int, 0)
m := map[int]int{
3: 200,
4: 200,
1: 100,
8: 800,
5: 500,
2: 200,
}
for key := range m {
keyList = append(keyList, key)
}
sort.Ints(keyList)
for _, key := range keyList {
fmt.Println(key, m[key])
}
}
程序输出
1 100
2 200
3 200
4 200
5 500
8 800
回答
如果想顺序遍历map,先把key放到切片排序,再按照key的顺序遍历map
4. map 中删除一个 key,它的内存会释放么?#
分析
考察map中key的删除原理,map删除key的时候是根据hash值找对应的槽位,找到对应的key删除,将key置为空,并且将对应的tophash置为emptyOne,如果后面没有任何数据了,则将emptyOne状态置为emptyReset,所以删除一个key,只是修改key对应存位置的值,并不会释放内存
回答
假设当前map的状态如下图所示,溢出桶2后面没有在接溢出桶,或者虽然溢出桶2后面接的溢出桶中没有数据,溢出桶2中有三个空槽,即第2、3、6处为emptyOne。

在删除了溢出桶1的key2和key4,以及溢出桶2的key7之后,对应map状态如下:

回答
不会释放。删除一个key,可以认为是标记删除,只是修改key对应内存位置的值为空,并不会释放内存,只有在查空这个map的时候,整个map的空间才会被垃圾回收后释放
5. 怎么处理对 map 进行并发访问? 有没有其他方案? 区别是什么?#
分析
主要考察对加锁运用熟悉程度以及对go语言中内置的sync.map的了解,要使用线程安全的map,一般有这两种方式
- 加锁
- sync.map
同时,要明确这两种方式的性能比较,sync.map在性能上要优于map加锁,因为sync.map在底层使用了2个map,read和dirty来提升性能,对read的操作时原子操作不用加锁,只有在对read操作不能满足要求时才会加锁操作dirty,这样就减少了加锁的场景,锁竞争频率会减小,所以性能会高于单纯的map加锁,在回答的时候要突出sync.map的read和dirty,以及锁竞争的频率。
回答
对map进行加读写锁或者是使用sync.map
和原始map+RWLock的实现并发的方式相比,减少了加锁对性能的影响。它做了一些优化:可以无锁访问read map,而且会优先操作read map,他若只操作read map就可以满足要求,那就不用去加锁操作write map(dirty),所以在某些特定场景中它发生锁竞争的频率会远远小于map+RWLock的实现方式
优点:
适合读多写少的场景
缺点:
写多的场景,会导致 read map 缓存失效,需要加锁,冲突变多,性能急剧下降