sync.Mutex 详解
Go 中的“锁”通常会从 基础使用 → 原理 → 性能 → 场景选择 四个层次来问。
如果是中高级 Go 岗位,锁相关问题出现频率非常高。
第一层:Mutex 基础
Q1:什么是 sync.Mutex?
回答:
Mutex(互斥锁)用于保护共享资源,同一时刻只允许一个 Goroutine 进入临界区。
var mu sync.Mutex
mu.Lock()
count++
mu.Unlock()
官实际上想确认:
-
知道为什么需要锁
-
知道竞态条件(Race Condition)
Q2:什么情况下需要加锁?
例如:
var count int
go func() {
count++
}()
多个 Goroutine 同时修改:
读取
修改
写入
不是原子操作。
需要:
mu.Lock()
count++
mu.Unlock()
Q3:如何发现竞态问题?
Go 自带工具:
go test -race
或者:
go run -race main.go
这是一个很常见的问题。
第二层:RWMutex
Q4:RWMutex 和 Mutex 的区别?
var rw sync.RWMutex
读:
rw.RLock()
rw.RUnlock()
写:
rw.Lock()
rw.Unlock()
特点:
多个读同时进行
写操作独占
Q5:什么时候用 RWMutex?
读远多于写:
例如:
缓存
配置中心
字典查询
不适合:
读写比例接近
写很多
因为维护读写锁本身也有成本。
加分回答
很多人会说:
读多写少用 RWMutex。
更进一步:
当写操作比较频繁时,RWMutex 不一定比 Mutex 快,因为需要维护读者计数和唤醒逻辑。
第三层:Mutex 原理
这是高级重点。
Q6:Mutex 底层是如何实现的?
Go 的 Mutex 主要包含:
type Mutex struct {
state int32
sema uint32
}
核心机制:
CAS
自旋
信号量阻塞
获取锁时:
第一阶段
CAS 抢锁:
Lock
↓
CAS成功
↓
获得锁
第二阶段
抢不到锁:
短时间自旋
即:
循环尝试获取
避免立刻进入内核态。
第三阶段
还抢不到:
挂起
进入等待队列
等待唤醒。
Q7:什么是自旋锁?
简单理解:
不停尝试获取锁
不睡眠
例如:
while(lock){
}
优点:
避免线程切换
缺点:
浪费CPU
第四层:锁的优化
Q8:为什么锁会影响性能?
因为:
阻塞
唤醒
上下文切换
缓存失效
都会产生成本。
Q9:如何减少锁竞争?
常见回答:
1. 缩小锁粒度
不要:
mu.Lock()
queryDB()
httpCall()
mu.Unlock()
应该:
data := queryDB()
mu.Lock()
cache = data
mu.Unlock()
2. 分段锁
例如:
一个大Map
拆成:
16个Map
16把锁
减少竞争。
3. 原子操作
简单计数:
atomic.AddInt64()
代替:
Mutex
第五层:死锁
必问。
Q10:什么是死锁?
两个协程互相等待。
g1:
Lock(A)
Lock(B)
g2:
Lock(B)
Lock(A)
结果:
g1等B
g2等A
永远无法继续。
Q11:如何避免死锁?
统一加锁顺序。
例如:
永远先锁A
再锁B
不要:
有时AB
有时BA
第六层:sync.Once
经常和锁一起问。
Q12:sync.Once 是什么?
确保只执行一次。
var once sync.Once
once.Do(func() {
initConfig()
})
即使:
100个 Goroutine
同时调用
也只执行一次。
第七层:sync.Map
Q13:sync.Map 和 map+Mutex 有什么区别?
普通:
map + Mutex
适合:
业务Map
频繁更新
sync.Map:
var m sync.Map
适合:
读多写少
Key稳定
例如:
配置缓存
对象缓存
第八层:Atomic
高级非常喜欢。
Q14:Atomic 和 Mutex 的区别?
Atomic:
atomic.AddInt64(&count, 1)
特点:
无锁
CPU指令保证
速度快
Mutex:
mu.Lock()
count++
mu.Unlock()
特点:
支持复杂逻辑
支持多个变量
经典问题
Atomic 能替代 Mutex 吗?
答案:
不能。
Atomic 只能解决:
单变量原子更新
例如:
count++
但无法保证:
a++
b++
整体一致性。
高频压轴题
Q15:Channel 和 Mutex 怎么选?
这是 Go 最喜欢的问题之一。
回答思路:
| 场景 | 推荐 |
|---|---|
| 状态共享 | Mutex |
| 消息传递 | Channel |
| Worker Pool | Channel |
| 缓存Map | Mutex |
| 任务调度 | Channel |
| 计数器 | Atomic |
可以总结为:
Channel 更适合表达 Goroutine 之间的协作关系;Mutex 更适合保护共享状态。实际项目中大量数据结构(缓存、连接池、Map)通常使用 Mutex,而任务流转、异步处理、生产者消费者模型更适合 Channel。
如果是一场 **P6/P7 级 Go **,锁相关最常出现的深入追问通常是:
-
Mutex 和 RWMutex 的底层实现。
-
Mutex 的自旋与饥饿模式(Starvation Mode)。
-
Atomic 的内存语义。
-
sync.Map 为什么比 map+RWMutex 快(或不快)。
-
Go Runtime 调度与锁竞争的关系。
-
如何定位线上锁竞争(pprof、mutex profile)。
这些往往能区分“会用 Go”和“理解 Go 运行时”的候选人。