如何正确使用Mutex同步修改共享状态的函数?runBlocking场景解析
正确在Kotlin协程中使用Mutex同步共享状态的方案
这个问题我之前帮不少开发者排查过,核心误区其实是混淆了Kotlin Mutex的阻塞式API和挂起式API——尤其是在runBlocking这种「阻塞线程来运行协程」的场景下,误用阻塞API就会导致你遇到的线程长时间卡住的问题。
先搞懂关键区别:Mutex的两种锁定方式
Kotlin协程的Mutex有两类API:
- 阻塞式:
lock()和unlock(),这两个方法会直接阻塞当前线程,直到锁被获取/释放。这种方式完全不适合协程场景,尤其是runBlocking,因为它会占用协程绑定的线程,导致其他协程无法调度。 - 挂起式:
withLock { ... },这是协程友好的挂起函数。当锁被占用时,它会释放当前线程,让其他协程可以继续执行,直到锁可用时再恢复执行。这才是我们应该在协程中使用的方式。
为什么runBlocking里用阻塞lock会出问题?
runBlocking的本质是「在当前线程上启动一个协程调度器,并且阻塞这个线程直到协程执行完毕」。如果你在runBlocking内部的协程里调用了mutex.lock(),这个线程会被直接卡住——哪怕其他协程也需要获取这个锁,它们也无法得到调度(因为线程被占着),最终导致线程长时间阻塞,甚至死锁。
正确用法示例(适用于所有协程启动方式)
不管你是用launch/async还是runBlocking启动协程,都应该用withLock来同步共享状态:
import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock // 共享状态 var sharedCounter = 0 val mutex = Mutex() fun main() = runBlocking { // 多线程调用场景:用Dispatchers.Default(多线程调度器)启动多个协程 val jobs = List(10) { launch(Dispatchers.Default) { repeat(1000) { // 用挂起式的withLock同步修改共享状态 mutex.withLock { sharedCounter++ } } } } jobs.forEach { it.join() } println("最终计数:$sharedCounter") // 应该输出10000 }
多线程调用场景的注意事项
- 不要依赖线程限制:你提到无法用线程限制方案,而Mutex的
withLock本身就是跨线程安全的——不管协程运行在哪个线程上,withLock都会保证同一时间只有一个协程能进入代码块,完全不需要限制线程。 - 避免在withLock里调用阻塞操作:虽然
withLock是挂起友好的,但如果在代码块里调用了阻塞线程的操作(比如Thread.sleep()),还是会占用线程,影响协程调度。如果必须用阻塞操作,建议用withContext(Dispatchers.IO)包裹,把阻塞操作放到IO调度器的线程池里。
错误用法示例(需要避免)
下面这种用阻塞式lock()/unlock()的写法,在runBlocking里会导致线程长时间阻塞,甚至死锁:
// 错误示例!不要这么写 fun main() = runBlocking { launch { mutex.lock() // 阻塞当前线程 try { sharedCounter++ // 如果这里有耗时操作,线程会一直被占着 } finally { mutex.unlock() } } }
总结一下:只要你始终使用mutex.withLock { ... }这个挂起式API,不管是在launch/async还是runBlocking启动的协程里,都能正确同步共享状态,不会出现线程长时间阻塞的问题。
内容的提问来源于stack exchange,提问作者user221256
相关产品推荐
相关产品推荐

