Go Mutex
Mutex(互斥锁 )是 Go 语言sync包中最核心的同步原语,用于解决并发资源竞争问题,保证同一时间只有一个 goroutine 能访问临界区资源。
一、介绍
Mutex 全称 Mutual Exclusion(互斥),是一种同步锁:
- 加锁(
Lock()):goroutine 获得锁后,其他 goroutine 调用Lock()会阻塞,直到锁被释放。 - 解锁(
Unlock()):只有持有锁的 goroutine 能解锁,否则会引发 panic。 - 核心作用:保护临界区(多个 goroutine 共享的资源 / 代码段),避免数据竞争导致的不可预期结果。
二、 底层原理
Go 1.20+ 版本
2.1 结构体
2.1.1 对外暴露的Mutex
go
type Mutex struct {
_ noCopy // 禁止值拷贝(编译期检查)
mu isync.Mutex // 实际的锁实现(内部包)
}
2.1.2 内部真正的 Mutex 实现
go
type Mutex struct {
state int32 // 锁的核心状态(包含locked/woken/starving等)
sema uint32// 信号量,用于goroutine阻塞/唤醒
}
2.13 状态常量(位掩码)
go
const (
// 1 << iota 表示按位左移,依次生成 1,2,4...
mutexLocked = 1 << iota // 第0位:锁是否被持有(1=锁定,0=未锁定)
mutexWoken // 第1位:是否有goroutine被唤醒(1=已唤醒,避免重复唤醒)
mutexStarving // 第2位:是否处于饥饿模式(1=饥饿模式,0=正常模式)
mutexWaiterShift = iota // 从第3位开始,存储等待锁的goroutine数量(等待者计数)
starvationThresholdNs = 1e6 // 饥饿模式阈值:1毫秒(1000000纳秒)
)
state 字段的二进制结构
二进制: 1 0 1 00010
↑ ↑ ↑ ↑
饥饿 唤醒 锁定 等待者数量(十进制2)
mutexLocked(第 0 位):标记锁是否被持有,是最基础的状态。mutexWoken(第 1 位):标记有 goroutine 正在自旋 / 唤醒,避免其他 goroutine 重复唤醒,减少竞争。mutexStarving(第 2 位):标记锁是否进入饥饿模式,解决优先级反转问题。mutexWaiterShift(值为 3):等待锁的 goroutine 数量, 从第 3 位开始存储,计算方式:等待数 = state >> mutexWaiterShift
2.2 正常 / 饥饿模式
| 模式 | 核心规则 |
|---|---|
| 正常模式 | 等待者按 FIFO 排队,但唤醒后的等待者需要和新到来的 goroutine 竞争锁(新 goroutine 有 CPU 优势) |
| 饥饿模式 | 锁直接交给等待队列头部的 goroutine,新 goroutine 不参与竞争,直接排队(保证公平性) |
| 模式切换 | 等待者等待时间超过 1ms → 进入饥饿模式;等待者是最后一个 / 等待时间 < 1ms → 切回正常模式 |
2.2.1 Lock () 方法
go
func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex.
// Fast path: 快速路径,CAS尝试直接获取未锁定的锁
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
// 竞态检测(仅开启-race时生效)
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
// 成功获取锁,直接返回
return
}
// Slow path (outlined so that the fast path can be inlined)
// Slow path: 慢速路径(锁已被持有/竞争),进入复杂逻辑
m.lockSlow()
}
逻辑: 优先走快速路径(CAS 直接抢锁),失败则进入慢速路径(自旋→阻塞→唤醒→模式切换),兼顾性能与公平性
2.2.2 lockSlow() 方法
go
func (m *Mutex) lockSlow() {
var waitStartTime int64 // 记录当前goroutine开始等待的时间(用于判断是否进入饥饿模式)
starving := false // 标记当前goroutine是否处于饥饿状态(等待超过1ms)
awoke := false // 标记当前goroutine是否被唤醒(避免重复自旋/阻塞)
iter := 0 // 自旋次数(控制自旋的最大次数,避免无限自旋)
old := m.state // 缓存当前锁的state状态(减少原子读取次数)
for {
// Don't spin in starvation mode, ownership is handed off to waiters
// so we won't be able to acquire the mutex anyway.
// if old&(mutexLocked|mutexStarving) == mutexLocked 锁被持有且非饥饿模式(饥饿模式不自旋)
// runtime_canSpin(iter) → 满足自旋条件(CPU核数>1、有空闲P、自旋次数<4等)
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// Active spinning makes sense.
// Try to set mutexWoken flag to inform Unlock
// to not wake other blocked goroutines.
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
// 标记当前goroutine已唤醒,避免重复设置
awoke = true
}
// 执行自旋(空转,消耗CPU但不阻塞,抢锁窗口期)
runtime_doSpin()
// 自旋次数+1
iter++
// 重新读取state(自旋期间可能被其他goroutine修改)
old = m.state
continue // 继续循环,尝试抢锁
}
new := old // 基于旧状态初始化新状态
// Don't try to acquire starving mutex, new arriving goroutines must queue.
// 非饥饿模式 → 尝试抢占锁(设置mutexLocked位)
if old&mutexStarving == 0 {
new |= mutexLocked // 标记锁为锁定状态(尝试抢锁)
}
// 锁被持有 或 处于饥饿模式 → 等待者数量+1
if old&(mutexLocked|mutexStarving) != 0 {
// 等待者数量左移3位后+1(等价于等待数+1)
new += 1 << mutexWaiterShift
}
// The current goroutine switches mutex to starvation mode.
// But if the mutex is currently unlocked, don't do the switch.
// Unlock expects that starving mutex has waiters, which will not
// be true in this case.
// 当前goroutine饥饿(等待>1ms)且锁被持有 → 切换为饥饿模式
if starving && old&mutexLocked != 0 {
new |= mutexStarving // 设置饥饿模式位
}
// 当前goroutine被唤醒 → 清除mutexWoken位(重置唤醒标记)
if awoke {
// The goroutine has been woken from sleep,
// so we need to reset the flag in either case.
// // 校验:如果新状态没有唤醒位,说明状态不一致,直接panic
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
// 清除唤醒位(等价于 new = new & ^mutexWoken)
new &^= mutexWoken
}
// CAS原子操作:尝试将state从old修改为new(抢锁核心)
if atomic.CompareAndSwapInt32(&m.state, old, new) {
//成功修改state后,判断是否已获取锁
// old&(mutexLocked|mutexStarving) == 0 → 锁未被持有且非饥饿,说明CAS直接抢到锁
if old&(mutexLocked|mutexStarving) == 0 {
break // locked the mutex with CAS 抢锁成功,退出循环
}
// If we were already waiting before, queue at the front of the queue.
// 以下是"未直接抢到锁"的逻辑:需要加入等待队列阻塞
// queueLifo=true → 把当前goroutine放到等待队列头部(LIFO),用于饥饿模式优先处理
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()// 记录开始等待的时间(纳秒)
}
// 阻塞当前goroutine,等待信号量(被Unlock()唤醒)
runtime_SemacquireMutex(&m.sema, queueLifo, 2)
// 唤醒后:判断是否饥饿(等待时间>1ms)
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
// 重新读取唤醒后的state
old = m.state
// 被唤醒后发现锁处于饥饿模式 → 直接获取锁并处理状态
if old&mutexStarving != 0 {
// If this goroutine was woken and mutex is in starvation mode,
// ownership was handed off to us but mutex is in somewhat
// inconsistent state: mutexLocked is not set and we are still
// accounted as waiter. Fix that.
// 校验:饥饿模式下的状态必须合法(锁未被持有、有等待者、无唤醒位)
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
// delta = mutexLocked(设置锁定位) - 1<<mutexWaiterShift(等待数-1)
delta := int32(mutexLocked - 1<<mutexWaiterShift)
// 退出饥饿模式的条件:当前goroutine不饥饿 或 是最后一个等待者
if !starving || old>>mutexWaiterShift == 1 {
// Exit starvation mode.
// Critical to do it here and consider wait time.
// Starvation mode is so inefficient, that two goroutines
// can go lock-step infinitely once they switch mutex
// to starvation mode.
delta -= mutexStarving // 清除饥饿模式位
}
atomic.AddInt32(&m.state, delta) // 原子修改state,完成锁获取
break // 抢锁成功,退出循环
}
// 被唤醒后处于正常模式 → 标记为唤醒状态,重置自旋次数,继续循环抢锁
awoke = true
iter = 0
} else {
// CAS失败(state被其他goroutine修改)→ 重新读取state,继续循环
old = m.state
}
}
// 竞态检测
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
}

graph TD
A["开始 lockSlow()"] --> B[初始化变量]
B --> C[循环开始]
C --> D{可以自旋?}
D -- 是 --> E[执行自旋操作]
E --> C
D -- 否 --> F[准备新状态: new = old]
F --> G{是否为饥饿模式?}
G -- 是 --> H[增加等待者数量]
G -- 否 --> I[尝试抢占锁: new |= mutexLocked]
I --> H
H --> J{锁是否被持有?}
J -- 是 --> K[增加等待者数量: new += 1<<mutexWaiterShift]
J -- 否 --> L{是否饥饿?}
K --> L
L -- 是 --> M[切换为饥饿模式: new |= mutexStarving]
L -- 否 --> N{是否被唤醒?}
M --> N
N -- 是 --> O[清除唤醒标志: new &^= mutexWoken]
N -- 否 --> P[CAS尝试修改状态]
O --> P
P --> Q{CAS成功?}
Q -- 否 --> R[重新读取状态: old = m.state]
R --> C
Q -- 是 --> S{直接获取锁?}
S -- 是 --> T[成功获取锁,退出循环]
S -- 否 --> U{已在等待?}
U -- 否 --> V[记录等待开始时间]
V --> W[阻塞等待]
U -- 是 --> W
W --> X{是否进入饥饿状态?}
X --> Y[重新读取状态: old = m.state]
Y --> Z{是否为饥饿模式?}
Z -- 是 --> AA[校验饥饿模式状态]
AA --> BB[计算状态变化: delta = mutexLocked - 1<<mutexWaiterShift]
BB --> CC{退出饥饿模式?}
CC -- 是 --> DD[清除饥饿模式: delta -= mutexStarving]
DD --> EE[原子更新状态]
CC -- 否 --> EE
EE --> FF[成功获取锁,退出循环]
Z -- 否 --> GG[设置唤醒状态: awoke = true]
GG --> HH[重置自旋次数: iter = 0]
HH --> C
T --> II[结束]
FF --> II
lockSlow() 核心行为:
- 自旋(Spin)
- 锁处于正常模式 (非饥饿),饥饿模式下锁会直接移交等待者,自旋无意义;
- 满足
runtime_canSpin条件- CPU 核心数 > 1(否则自旋浪费 CPU)
- 当前 goroutine 所在的 P(处理器)有空闲的 M(线程)
- 自旋次数 iter < 4(默认最大自旋次数,避免无限自旋)
- CAS 修改 state 并处理阻塞 / 唤醒
- (1)CAS 成功但未直接抢锁 → 进入阻塞
- queueLifo:
waitStartTime != 0表示当前 goroutine 之前已经等待过,此时会被放到等待队列头部(LIFO),优先被唤醒(解决饥饿问题)- 首次等待则放到队列
尾部(FIFO)
- queueLifo:
- (2)被唤醒后判断是否饥饿
runtime_nanotime()-waitStartTime>starvationThresholdNs:
计算等待时间是否超过1ms(starvationThresholdNs=1e6纳秒),如果是则标记starving=true。
- (3)饥饿模式下的锁获取(公平性保障)
- 饥饿模式下,锁会直接移交 给当前 goroutine,无需竞争`。避免 "新 goroutine 无限抢锁,老 goroutine 饿死 "
delta = mutexLocked - 1<<mutexWaiterShift:设置锁定位(抢锁)+ 等待数 - 1- 如果是最后一个等待者 / 当前 goroutine 不饥饿 ,清除饥饿模式位(切回正常模式)
- 原子修改 state 后,直接 break 抢锁成功
- 饥饿模式下,锁会直接移交 给当前 goroutine,无需竞争`。避免 "新 goroutine 无限抢锁,老 goroutine 饿死 "
- (4)正常模式下的唤醒处理
- 标记 awoke=true,重置自旋次数 iter=0,回到循环开头重新自旋抢锁
- 阻塞等待:自旋失败后,通过 sema 信号量将 goroutine 加入等待队列,进入阻塞状态(让出 CPU)
- 模式切换 :被唤醒后检查等待时间,超过
1ms则切换到饥饿模式,保证公平性
2.2.3 Unlock() 方法
go
func (m *Mutex) Unlock() {
// 竞态检测逻辑
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
// Fast path: drop lock bit.
// 快速路径(Fast Path):无等待者的锁释放
// 用 atomic.AddInt32(原子加法)将 state 减去 mutexLocked(值为 1),本质是清除第 0 位的
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
// Outlined slow path to allow inlining the fast path.
// To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
m.unlockSlow(new)
}
}
new = 0→- 说明没有等待者、非饥饿模式、无唤醒的 goroutine,锁已完全释放,无需后续操作,直接返回
new != 0→- 有等待锁的 goroutine(等待者数量 > 0);
- 锁处于饥饿模式(mutexStarving 位为 1);
- 有被唤醒的 goroutine(mutexWoken 位为 1)
2.2.4 unlockSlow() 方法
go
func (m *Mutex) unlockSlow(new int32) {
// 非法解锁校验
// new 是释放锁定位后的 state 值
// new + mutexLocked 还原释放前的 state
// & mutexLocked 提取还原后 state 的第 0 位(锁定位)
// 若结果为 0,说明释放前锁的锁定位就是 0(未加锁),属于非法解锁
if (new+mutexLocked)&mutexLocked == 0 {
fatal("sync: unlock of unlocked mutex")
}
// 非饥饿
if new&mutexStarving == 0 {
// 缓存当前 state,减少原子读取
old := new
for {
// If there are no waiters or a goroutine has already
// been woken or grabbed the lock, no need to wake anyone.
// In starvation mode ownership is directly handed off from unlocking
// goroutine to the next waiter. We are not part of this chain,
// since we did not observe mutexStarving when we unlocked the mutex above.
// So get off the way.
// old>>mutexWaiterShift == 0 → 无等待者
// old&(mutexLocked|mutexWoken|mutexStarving) != 0 → 锁被持有/有唤醒的 goroutine/进入饥饿模式
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// 计算新 state:等待者数量-1 + 标记 mutexWoken(避免重复唤醒)
// Grab the right to wake someone.
new = (old - 1<<mutexWaiterShift) | mutexWoken
// CAS 原子修改 state:保证多 goroutine 下状态一致
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// // 唤醒一个等待的 goroutine(非 FIFO,正常模式下竞争抢锁)
runtime_Semrelease(&m.sema, false, 2)
return
}
// CAS 失败(state 被其他 goroutine 修改)→ 重新读取 state 重试
old = m.state
}
} else {
// Starving mode: handoff mutex ownership to the next waiter, and yield
// our time slice so that the next waiter can start to run immediately.
// Note: mutexLocked is not set, the waiter will set it after wakeup.
// But mutex is still considered locked if mutexStarving is set,
// so new coming goroutines won't acquire it.
// 饥饿模式的核心设计是公平优先,锁直接移交等待最久的 goroutine,而非让新 goroutine 竞争
// true 表示按 FIFO 唤醒(严格唤醒等待队列头部的 goroutine,即等待最久的那个)
// 被唤醒的 goroutine 无需竞争,直接获取锁,解决 "老 goroutine 饿死" 的问题
runtime_Semrelease(&m.sema, true, 2)
}
}

go
graph TD
A[进入unlockSlow] --> B[非法解锁校验,失败则fatal]
B --> C{是否饥饿模式?};
C -->|正常模式| D[初始化old=new,进入循环]
D --> E{无等待者/锁被持有/有唤醒goroutine?};
E -->|是| F[返回,无需唤醒]
E -->|否| G[计算new=等待数-1+标记唤醒位]
G --> H{CAS修改state成功?};
H -->|是| I[唤醒一个goroutine(非FIFO),返回]
H -->|否| J[更新old=最新state,回到E]
C -->|饥饿模式| K[唤醒队列头部goroutine(FIFO),返回]
unlockSlow() 核心流程
- 非法解锁校验 :
校验 "解锁的是未加锁的 Mutex"(非法操作),直接终止程序 - 正常模式逻辑 :仅在 "有等待者、无唤醒 goroutine " 时唤醒一个 goroutine(
非 FIFO),优先保证性能 - 饥饿模式逻辑 :严格按
FIFO唤醒队列头部的 goroutine,锁直接移交,保证公平性;
三、使用场景
1. 保护共享变量的读写(最基础场景)
当多个 goroutine 同时读写同一个变量时,必须用 Mutex 避免数据竞争,比如计数器、全局配置、缓存等。
go
package main
import (
"sync"
"fmt"
)
var (
count int
mu sync.Mutex
wg sync.WaitGroup
)
func increment() {
defer wg.Done()
for i := 0; i < 1000; i++ {
mu.Lock() // 加锁保护count
count++ // 临界区:共享变量修改
mu.Unlock() // 解锁
}
}
func main() {
wg.Add(2)
go increment()
go increment()
wg.Wait()
fmt.Println("最终计数:", count) // 正确输出2000(无锁会小于2000)
}
2. 保护复杂数据结构的操作
对于 map、slice 等非线程安全的复合数据结构,若多个 goroutine 同时执行 "读 + 写" 或 "写 + 写" 操作,必须用 Mutex 包裹完整的操作逻辑(而非单个步骤)
go
package main
import (
"fmt"
"sync"
)
type SafeMap struct {
m map[string]int
mu sync.Mutex
}
// 新增/更新key(写操作)
func (sm *SafeMap) Set(key string, val int) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.m[key] = val
}
// 获取key(读操作)
func (sm *SafeMap) Get(key string) (int, bool) {
sm.mu.Lock()
defer sm.mu.Unlock()
val, ok := sm.m[key]
return val, ok
}
func main() {
sm := &SafeMap{m: make(map[string]int)}
var wg sync.WaitGroup
// 10个goroutine并发写
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
sm.Set(fmt.Sprintf("key%d", n), n)
}(i)
}
wg.Wait()
// 读取验证
for i := 0; i < 10; i++ {
val, ok := sm.Get(fmt.Sprintf("key%d", i))
fmt.Printf("key%d: %d, exists: %t\n", i, val, ok)
}
}
四、注意事项
1. 必须保证 Lock/Unlock 配对(最核心)
错误场景 :加锁后未解锁(比如 panic 导致 Unlock 未执行),会导致其他 goroutine 永久阻塞(死锁);
正确做法 :加锁后立即用 defer mu.Unlock(),确保函数退出时必解锁
2. 禁止重复加锁 / 解锁
- 重复加锁 :同一 goroutine 连续 Lock() 会直接死锁
- 重复解锁:未加锁 / 已解锁时调用 Unlock() 会 panic
3. 缩小锁粒度(性能优化核心)
Mutex 会串行化临界区代码,锁粒度越大,并发性能越差。只锁必要的代码段,而非整个函数
4. 禁止拷贝 Mutex(编译期检查)
Mutex 包含noCopy结构体,go vet 会检查是否值拷贝,拷贝后原锁的状态会丢失,导致同步失效
五、Mutex 和 RWMutex 的区别
| 特性 | Mutex | RWMutex |
|---|---|---|
| 加锁规则 | 同一时间仅一个 goroutine 加锁 | 读锁:多 goroutine 并发;写锁:仅一个 goroutine |
| 互斥关系 | 读写 / 写写 / 读读均互斥 | 读写 / 写写互斥,读读不互斥 |
| 性能 | 简单无额外开销 | 读多写少场景性能更高,有状态管理开销 |
| 缺点 | 读并发性能差 | 写锁阻塞所有读锁,状态管理有开销 |
| 适用场景 | 读写频率相当、写操作占比高 | 读多写少(如配置读取、缓存查询) |