一句话结论
Go sync.Mutex 是CAS + 信号量实现的互斥锁,有正常模式和饥饿模式两种状态。正常模式下等待者自旋+排队,饥饿模式下按 FIFO 直接交接,防止尾端 Goroutine 饿死。
核心原理
底层结构
type Mutex struct {
state int32 // 低 3 位: locked(1) + woken(1) + starving(1)
// 高 29 位: 等待者数量
sema uint32 // 信号量(runtime_Semacquire/Semrelease 阻塞/唤醒)
}
状态位
正常模式(Normal Mode)
尝试加锁
├── CAS(0 → locked): 成功 → 获得锁
├── 自旋: 锁被持有但 holder 可能在另一个 P 上运行
│ └── 自旋条件: 多核 + GOMAXPROCS>1 + P 本地队列空 + 自旋次数<4
├── 自旋后仍未获得 → 等待者+1 → Semacquire 阻塞
└── 被唤醒 → 重新竞争锁
正常模式的特点: 被唤醒的 Goroutine 不直接获得锁,需要和新到达的 Goroutine 竞争——新来者在 CPU 上运行有优势,可能抢走锁。
饥饿模式(Starvation Mode)
触发条件:
等待者等待超过 1ms
进入饥饿模式:
mutexStarving = 1
锁释放时:
直接通过信号量交接给 FIFO 队列头部的等待者(不和新来者竞争)
退出饥饿模式:
当前等待者是队列最后一个,或等待时间 < 1ms
自旋条件
同时满足以下全部条件才自旋:
1. 多核 CPU
2. GOMAXPROCS > 1
3. 当前 P 的本地运行队列为空
4. 自旋次数 < 4
自旋时: 占用 CPU 但不做有用工作(busy-waiting)
项目中的应用
在 电商交易系统 中,秒杀库存的本地缓存用 Mutex 保护:
type StockCache struct {
mu sync.RWMutex
cache map[string]int
}
func (sc *StockCache) Get(productID string) (int, bool) {
sc.mu.RLock()
defer sc.mu.RUnlock()
stock, ok := sc.cache[productID]
return stock, ok
}
// 读多写少 → RWMutex,比 Mutex 并发度更高
优点与缺点
异常与边界
不可重入
mu := sync.Mutex{}
mu.Lock()
mu.Lock() // ❌ 死锁!Go 的 Mutex 不是可重入的
// 如果需要可重入锁,需要自己实现(记录持有者 Goroutine ID)
复制 Mutex
var mu sync.Mutex
mu.Lock()
mu2 := mu // ❌ 复制了锁的内部状态(state, sema)
mu2.Unlock() // 未定义行为
// go vet 会检测: "call of mu2.Unlock copies lock value"
WaitGroup 误用
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
// work
}(i) // ✅ i 作为参数传入
}
wg.Wait()
// ❌ 常见错误:
// 1. wg.Add 写在 goroutine 内部(可能在 Wait 之后执行)
// 2. wg.Add 的总数 < wg.Done 的次数 → 负数 panic
// 3. wg 被复制传递
高频面试问题
Q: Mutex 正常模式和饥饿模式的区别?
30 秒回答: 正常模式下被唤醒的 Goroutine 要和新来的竞争锁——新来者在 CPU 上有优势,可能抢走。饥饿模式下释放锁直接交给等待最久的 Goroutine(FIFO),防止尾端无限等待。等待超过 1ms 触发饥饿模式。
继续追问:
"饥饿模式什么时候退出?" → 当前拿到锁的 Goroutine 是队列最后一个,或它的等待时间 < 1ms。
Q: 自旋有什么代价?
30 秒回答: 自旋占用 CPU 但不做有用工作。好处是避免了 Goroutine 挂起/唤醒的上下文切换开销。Go 限制自旋次数 ≤ 4 且只在多核空闲 P 时才自旋,平衡收益和代价。
最小实验
// race detector 检测数据竞争
// go run -race main.go
func main() {
var count int
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
count++ // ❌ 数据竞争
}()
}
wg.Wait()
}
// 输出: WARNING: DATA RACE
速记
Mutex = CAS + 信号量。正常模式=竞争,饥饿模式=FIFO 防饿死。自旋 ≤ 4 次 + 多核空闲 P。不可重入。不可复制。RWMutex 读多写少更优。