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 **,锁相关最常出现的深入追问通常是:

  1. Mutex 和 RWMutex 的底层实现。

  2. Mutex 的自旋与饥饿模式(Starvation Mode)。

  3. Atomic 的内存语义。

  4. sync.Map 为什么比 map+RWMutex 快(或不快)。

  5. Go Runtime 调度与锁竞争的关系。

  6. 如何定位线上锁竞争(pprof、mutex profile)。

这些往往能区分“会用 Go”和“理解 Go 运行时”的候选人。