Skip to content

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 上使用传统锁会导致线程池耗尽死锁。
  • 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 / synchronizedKotlin 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_stateflowMutex 挂起锁竞态对比、StateFlow / SharedFlow 语义验证

复习检查题 ​

  1. 在 Kotlin 协程中,为什么 Mutex 拿不到锁时不会阻塞物理线程?

    答:因为 Mutex.lock() 被实现为了一个 suspend 挂起函数。当协程抢锁失败时,它不会像传统 Java 锁那样调用操作系统的线程挂起 API,而是将其续体(Continuation)加入到 Mutex 的等待队列中,并立刻出让当前线程给其他协程使用,实现了非阻塞的高效等待。

  2. 为什么在同一个协程内嵌套调用同一个 Mutex 的 withLock 会引发死锁?

    答:因为 Kotlin 的 Mutex 默认是不支持可重入的 (Non-reentrant)。当同一个协程在 outer() 中拿到锁后,在 inner() 中再次尝试获取该 Mutex 时,Mutex 会误判定为未拿锁并将其挂起;而解锁动作必须在 outer() 结束时才会触发,从而形成了自己等待自己的永久挂起死锁。

速记 ​

  • 锁选型:协程用 Mutex 挂起锁,物理线程用 ReentrantLock,主线程严禁传统阻塞锁。
  • 死锁禁忌:Mutex 不可重入!严禁在同协程嵌套调用同一个 Mutex 的 lock。
  • 推荐写术:统一使用 mutex.withLock { ... } 表达式,保障异常时自动释放锁。

站点构建时间:2026/8/24 23:43:17