Appearance
Kotlin 协程 Mutex 互斥锁、共享变量与无锁并发
回到总览:并发与运行时底座
相关模块:Kotlin 协程 Dispatchers、CoroutineContext 与构建器 · Atomic* / CAS / LongAdder
一句话定义
Mutex 是 Kotlin 协程提供的非阻塞挂起互斥锁 (Non-blocking Suspending Lock);当某个协程未抢到锁时,它不会像传统 Java ReentrantLock 那样阻塞物理线程,而是挂起当前协程并出让线程给其他任务,从而在共享变量并发访问时实现极高效的线程安全保护。
代码索引
对应 Lab:coroutine_mutex_stateflow
为什么需要
- 为什么在 Kotlin 协程中严禁使用 Java 的
synchronized或ReentrantLock.lock()来保护挂起函数?- 一句话答:传统 Java 锁在等待时会直接物理阻塞线程;如果在
Dispatchers.Main上使用传统锁,会导致主线程假死卡顿;在Dispatchers.Default上使用传统锁会导致线程池耗尽死锁。
- 一句话答:传统 Java 锁在等待时会直接物理阻塞线程;如果在
Mutex与传统ReentrantLock最大的区别是什么?- 一句话答:
Mutex.lock()是一个suspend挂起函数,拿不到锁时只挂起协程、不阻塞线程;而ReentrantLock.lock()是阻塞函数,会强行挂起 CPU 物理线程。
- 一句话答:
底层机制
1. Java 物理锁 vs Kotlin 协程 Mutex 挂起锁对比
图注补充:Mutex: 1. Mutex.withLock {} 成功拿到锁;Mutex: 3. 调用 mutex.lock() 抢锁失败;Mutex: 4. 执行完毕解锁 mutex.unlock();CoroutineB: 5. 唤醒协程 B 恢复执行。
性能与行为对比表
| 维度 | Java ReentrantLock / synchronized | Kotlin Mutex |
|---|---|---|
| 等待行为 | 物理阻塞线程 (Thread Blocked) | 挂起当前协程 (Coroutine Suspended) |
| 线程利用率 | 极低(线程进入 TIMED_WAITING,挂起 CPU) | 极高(线程立刻被放回执行其他协程) |
| 可重入性 | 可重入 (Reentrant) | 默认不可重入 (Reentrant 会死锁!) |
| 适用场景 | 线程间同步、底层 JUC 框架 | 协程间共享状态互斥保护 |
代码示例与防死锁陷阱
kotlin
class SafeCounter {
private val mutex = Mutex()
private var counter = 0
suspend fun inc(): Int = mutex.withLock {
// withLock 自动在 try-finally 中释放锁
counter++
counter
}
}常见误配、事故后果与排障
1. 事故:误以为 Mutex 是可重入锁导致死锁崩溃
- 误配原因:在持有
mutex的代码块内部再次调用了需要同一个mutex的方法:
kotlin
val mutex = Mutex()
suspend fun outer() = mutex.withLock {
inner() // 死锁!Mutex 默认不可重入!
}
suspend fun inner() = mutex.withLock {
// 永久挂起无法拿到锁
}- 排障与修法:重构代码剥离内部的加锁逻辑,或使用单线程调度器(
newSingleThreadContext)代替 Mutex。
对应实验
各章节内已嵌入跳转链接;此处汇总全部 Lab:
| Lab | 说明 |
|---|---|
| coroutine_mutex_stateflow | Mutex 挂起锁竞态对比、StateFlow / SharedFlow 语义验证 |
复习检查题
在 Kotlin 协程中,为什么
Mutex拿不到锁时不会阻塞物理线程?答:因为
Mutex.lock()被实现为了一个suspend挂起函数。当协程抢锁失败时,它不会像传统 Java 锁那样调用操作系统的线程挂起 API,而是将其续体(Continuation)加入到 Mutex 的等待队列中,并立刻出让当前线程给其他协程使用,实现了非阻塞的高效等待。为什么在同一个协程内嵌套调用同一个
Mutex的withLock会引发死锁?答:因为 Kotlin 的
Mutex默认是不支持可重入的 (Non-reentrant)。当同一个协程在outer()中拿到锁后,在inner()中再次尝试获取该 Mutex 时,Mutex 会误判定为未拿锁并将其挂起;而解锁动作必须在outer()结束时才会触发,从而形成了自己等待自己的永久挂起死锁。
速记
- 锁选型:协程用 Mutex 挂起锁,物理线程用 ReentrantLock,主线程严禁传统阻塞锁。
- 死锁禁忌:Mutex 不可重入!严禁在同协程嵌套调用同一个 Mutex 的 lock。
- 推荐写术:统一使用
mutex.withLock { ... }表达式,保障异常时自动释放锁。