Go sync.Mutex 原子互斥锁 底层实现
一、Mutex 到底是什么?
一句话:
Mutex = 一个原子状态位 + 排队等待机制 = 保证同一时间只有一个 goroutine 能执行
Go 1.18+ 之后的 Mutex 是 ** 完全基于原子操作(atomic)** 实现的,没有用系统级锁,性能极高。
口诀
一抢、二自旋、三休眠、四唤醒
- 抢:用 CAS 原子操作抢锁
- 自旋:抢不到就原地自旋一小会儿
- 休眠:还抢不到,进入队列休眠(不占 CPU)
- 唤醒:锁释放时,唤醒队列第一个人
二、Mutex 核心源码结构
文件:sync/mutex.go
1. Mutex 结构体(极简!)
// Mutex 互斥锁
// 只有一个 32 位整型状态位,所有逻辑全靠它!
type Mutex struct {
// 锁状态:
// 第 0 位:是否上锁 1=已锁 0=未锁
// 第 1 位:是否有唤醒的goroutine
// 第 2 位:是否是饥饿模式
// 其余位:等待队列的goroutine数量
state int32
// 信号量:用于休眠/唤醒排队的goroutine
sema uint32
}
2. 常量定义
const (
mutexUnlocked = 0 // 未锁
mutexLocked = 1 // 第0位:已锁
mutexWoken = 2 // 第1位:被唤醒
mutexStarving = 4 // 第2位:饥饿模式
)
三、Lock () 加锁核心源码
面试点
// Lock 加锁
func (m *Mutex) Lock() {
// ====================== 快速路径:第一次直接抢锁 ======================
// CAS 原子操作:如果 state 是 0(未锁),就设置为 1(已锁)
// 成功:直接拿到锁,返回
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
// ====================== 慢路径:抢不到,进入循环等待 ======================
m.lockSlow()
}
// lockSlow 慢路径:自旋 + 休眠 + 饥饿模式
func (m *Mutex) lockSlow() {
// 等待开始时间
var waitStartTime int64
// 是否饥饿
starving := false
// 是否被唤醒
awoke := false
// 自旋次数
iter := 0
// 缓存当前状态
state := m.state
// 死循环,直到拿到锁
for {
// ====================== 步骤1:尝试自旋(不休眠,节省上下文切换) ======================
// 允许自旋条件:
// 1. 不是饥饿模式
// 2. 可以自旋(CPU多核、自旋次数少)
if !starving && canSpin(iter) {
// 自旋!原地循环,不释放CPU
iter++
// 主动让出CPU时间片,等待锁释放
procyield(30)
continue
}
// ====================== 步骤2:准备休眠,修改状态位 ======================
// 新状态 = 已锁
newState := state | mutexLocked
// 如果是饥饿模式,加上饥饿标记
if starving {
newState |= mutexStarving
}
// 如果是被唤醒的,清除唤醒标记
if awoke {
newState &^= mutexWoken
}
// ====================== 步骤3:原子更新状态,加入等待队列 ======================
if atomic.CompareAndSwapInt32(&m.state, state, newState) {
// 计算等待时间
waitStartTime = runtime_nanotime()
// 休眠!挂起goroutine,等待信号量唤醒
runtime_SemacquireMutex(&m.sema, waitStartTime)
// ====================== 步骤4:被唤醒,重新开始抢锁 ======================
awoke = true
// 重置自旋次数
iter = 0
}
// 重新读取最新状态
state = m.state
}
}
四、Unlock () 解锁源码
// Unlock 解锁
func (m *Mutex) Unlock() {
// ====================== 快速路径:原子解锁 ======================
// 减去锁标记
state := atomic.AddInt32(&m.state, -mutexLocked)
// 如果没有等待的goroutine,直接结束
if state != 0 {
// 有等待者,进入慢路径唤醒
m.unlockSlow(state)
}
}
// unlockSlow 唤醒等待的goroutine
func (m *Mutex) unlockSlow(state int32) {
for {
// 唤醒等待队列中的第一个goroutine
state &^= mutexWoken
atomic.CompareAndSwapInt32(&m.state, state, state)
// 信号量释放,唤醒休眠的goroutine
runtime_Semrelease(&m.sema)
return
}
}
五、Mutex 流程图
Lock () 加锁全流程
Unlock () 解锁流程
六、总结
1. Mutex 完全基于原子操作(CAS)实现
- 没有系统调用
- 完全在用户态
- 速度极快
2. 加锁四步走
- CAS 抢锁(最快)
- 抢不到 → 自旋(不休眠)
- 还抢不到 → 休眠排队(释放 CPU)
- 唤醒 → 重新抢锁
3. 解锁两步走
- 原子解锁
- 唤醒第一个排队者
4. 核心特点
- 轻量级:只有一个 int32 状态
- 高性能:无系统调用损耗
- 公平性:等待队列先进先出
- 饥饿模式:防止等待太久,打破不公平性
七、面试问题
1. Mutex 是怎么实现的?
基于原子 CAS 操作 + 自旋 + 信号量休眠唤醒,完全用户态实现,无系统锁。 这个要把mutex结构、上锁的过程都要说清楚
2. 自旋是什么?
抢不到锁时,原地循环一小会儿,不休眠,避免上下文切换开销。 因为有可能上一个程序很快的释放锁,如果保存现场切换上下文会降低拿锁的效率
3. 为什么 Mutex 快?
大部分场景能快速 CAS 抢到锁,无锁竞争时无任何损耗。
4. 饥饿模式干嘛用?
防止某些 goroutine 一直抢不到锁,保证公平性。 因为自旋的程序一直在cpu上,竞争时很大可能会优先拿到锁,如果竞争自旋的协程过多会导致进入等待队列的协程一直拿不到锁,饥饿模式就是为了应对这样不公平的场景出现的,当进入饥饿模式后会优先给等待时间过长的协程。 如果拿锁的时间小于饥饿时间或者等待的队列中没有了要获取锁的协程,自动退出及饥饿模式
5. Lock () 慢路径做了什么?
自旋 → 休眠排队 → 唤醒重试