Sync 面试题
Sync 面试题
1. 除了 mutex 以外还有那些方式安全读写共享变量?
还有信号量、通道(Channel)、原子操作(atomic)等方式可以安全读写共享变量。
2. Go 语言是如何实现原子操作的?
Go 语言依赖底层 CPU 硬件提供的原子指令.
具体来说,Go 的 sync/atomic 包中的函数,在编译时会被编译器识别,并直接转换成对应目标硬件平台的单条原子指令。
3. 聊聊原子操作和锁的区别?
原子操作是 CPU 硬件层面的“微观”机制,它保证对单个数据的单次读改写操作是绝对不可分割的,性能极高
锁(如 mutex)是软件层面的“宏观”机制,它通过互斥访问来保护一段代码或数据结构,适用于更复杂的场景,但性能较原子操作低
4. Go 语言互斥锁 mutex 底层是怎么实现的?
mutex 底层是通过原子操作加信号量来实现的
互斥锁对应的底层结构是 sync.Mutex 结构体
1 | type Mutex struct { |
state 代表锁的状态,0 表示未锁定,1 表示锁定,2 表示有等待的 goroutine。sema 是一个信号量,用于阻塞和唤醒等待的 goroutine。
5. Mutex 有几种模式?
Mutex 有两种模式:正常模式和饥饿模式。
-
正常模式:默认模式。
- 特点:
- 新来的 goroutine 可以参与抢锁
- 等待队列中的 goroutine 不一定先拿到锁
- 优点:
- 吞吐量高
- 缺点:
- 可能饿死
- 特点:
-
饥饿模式:如果某 goroutine 等待超过 1ms,mutex 会进入饥饿模式。
- 此时:锁直接交给等待队列头部 goroutine
- 新 goroutine 不会自旋,必须排到队尾
6. 在 Mutex 上自旋的 goroutine 会占用太多资源吗?
并不会,因为:
- 有严格的次数和时间限制,通常指持续几十纳秒
- 自旋仅在特定条件下才会发生
7. Mutex 已经被一个 Goroutine 获取了,其它等待中的 Goroutine 们只能一直等待。那么等这个锁释放后,等待中的 Goroutine 中哪一个会优先获取 Mutex?
正常模式下,锁分配是“不公平”的,当锁释放时,等待队列中第一个 goroutine 会被唤醒,但不一定能拿到锁,可能被后面新来的 goroutine 抢先获取。
饥饿模式下,锁分配是“公平”的,等待队列中第一个 goroutine 会被唤醒并直接获取锁。
8. sync.Once 的作用是什么,讲讲它的底层实现原理?
sync.once 的作用是确保一个函数在程序生命周期内,无论在多少个 goroutine 中被调用,都只会被执行一次。
它常用于单例对象的初始化或一些只需要执行一次的全局配置加载
sync.0nce 保证代码段只执行 1 次的原理主要是其内部维护了一个标识位,当它 == 0 时表示还没执行过函数,此时会加锁修改标识位,然后执行对应函数。后续再执行时发现标识位 != 0,则不会再执行后续动作了
Once其实是一个结构体
1 | type Once struct { |
9. WaitGroup 是怎样实现协程等待?
WaitGroup 实现等待,本质上是一个原子计数器和一个信号量的协作。
调用 Add 会增加计数值,Done 会减计数值。而 Wait 方法会检查这个计数器,如果不为零,就利用信号量将当前 goroutine 高效地挂起。直到最后一个 Done 调用将计数器清零,它就会通过这个信号量,一次性唤醒所有在 Wait 处等待的goroutine,从而实现等待目的。
waitGroup 结构体定义如下:
1 | type WaitGroup struct { |
10. 讲讲 sync.Map 的底层原理?
sync.Map 的底层核心是“空间换时间”,通过两个 Map(read和dirty)的冗余结构,实现“读写分离”最终达到针对特定场景的“读”操作无锁优化。
它的 read 是一个只读的 map,提供无锁的并发读取,速度极快。
写操作则会先操作一个加了锁的、可读写的dirty map。当 dirty map 的数据积累到一定程度,或者 read map中没有某个 key 时,sync.Map 会将 dirty map 里的数据“晋升”并覆盖掉旧的 read map,完成一次数据同步。
sync.Map 的结构定义:
1 | type Map struct { |
11. read map 和 dirty map 之间有什么关联?
read map 是 dirty map 的一个不完全、且可能是过期的只读快照
dirty map 则包含了所有的最新数据
12. 为什么要设计 nil 和 expunged 两种删除状态?
nil 和 expunged 本质上是在区分:
| 状态 | 含义 |
|---|---|
| nil | 逻辑删除,可恢复 |
| expunged | 物理删除,不可直接恢复 |
这是 sync.Map 为了实现:
- 无锁读
- 延迟删除
- 降低锁竞争
- 减少 map 重建
- 提高高并发性能
而设计的“双阶段删除机制”。
13. sync.Map 适用的场景
适合读多写少的场景
参考资料: