0赞
赏
赞赏
更多好文
一把为线程设计的锁,困不住会“瞬移”的协程
——理解并发模型差异,避开协程同步陷阱
🌰 问题重现:一段“看似合理”的代码
val lock = Any()
var counter = 0
// 错误示范:在协程中使用 synchronized + 挂起点
GlobalScope.launch {
repeat(100) {
synchronized(lock) {
counter++ // 临界区操作
delay(10) // ⚠️ 挂起点!协程在此处挂起
println(counter)
}
}
}
运行结果:
❌ 抛出 IllegalMonitorStateException
❌ 或程序卡死(死锁)
❌ 计数结果混乱(锁未生效)
许多从 Java 转向 Kotlin 的开发者踩过这个坑:“我加了锁,为什么协程还是乱套了?”
🔑 核心矛盾:锁的“线程绑定” vs 协程的“线程无关”
| 特性 | Java 传统锁(synchronized / ReentrantLock) | Kotlin 协程 |
|---|---|---|
| 作用域 | 绑定到操作系统线程 | 绑定到协程自身 |
| 加锁/解锁 | 必须由同一线程完成 | 可在任意线程挂起/恢复 |
| 挂起行为 | 挂起 ≠ 释放锁(锁仍被原线程持有) | 挂起后可能在新线程恢复执行 |
| 设计哲学 | 阻塞式线程同步 | 非阻塞式协作调度 |
🌪️ 问题发生时发生了什么?
- 协程在 Thread A 执行
synchronized(lock),成功加锁 - 执行到
delay(10)时挂起,Thread A 释放(去执行其他任务) - 锁仍被 Thread A 持有(Java 锁不会因协程挂起而释放!)
- 协程恢复时被调度到 Thread B
- Thread B 尝试执行
monitorexit(退出 synchronized 块)→ 抛出异常(当前线程未持有锁)
💡 本质:Java 锁锁的是“线程”,而协程会“换线程”。锁没“失效”,而是被错误使用。
✅ 正确姿势:用协程友好的同步原语
方案一:kotlinx.coroutines.sync.Mutex(首选)
val mutex = Mutex()
var counter = 0
launch {
repeat(100) {
mutex.withLock { // 协程感知:挂起时自动保留“锁所有权”
counter++
delay(10) // ✅ 安全:恢复后仍在同一逻辑协程中
println(counter)
}
}
}
- ✅ 锁与协程绑定,而非线程
- ✅ 支持在锁内安全挂起/恢复
- ✅ 无 IllegalMonitorStateException 风险
方案二:withContext + 单线程调度器(场景受限)
withContext(Dispatchers.Default.limitedParallelism(1)) {
// 所有操作在同一线程池内串行执行
counter++
delay(10)
}
适用:简单串行场景,但灵活性低,不推荐复杂同步。
❌ 避坑提醒
ReentrantLock.withLock { ... }(kotlinx 扩展)要求块内无挂起点!若含delay会编译报错。- 永远不要在
synchronized/lock()块内调用挂起函数!
💡 深度思考:为什么设计如此?
| 维度 | Java 锁 | 协程 Mutex |
|---|---|---|
| 目标 | 保护线程级共享状态 | 保护协程级共享状态 |
| 调度模型 | 线程阻塞(OS 调度) | 协程挂起(用户态调度) |
| 哲学 | “锁住线程” | “锁住逻辑流” |
Kotlin 协程的设计初衷是避免线程阻塞,提升并发效率。若强行用线程锁,反而:
- 造成线程池饥饿(锁持有线程被长期占用)
- 破坏协程非阻塞优势
- 引入隐蔽的死锁风险
🌰 比喻:
Java 锁像“需要本人持身份证开门的闸机”,
协程像“拥有电子通行证的瞬移者”——
他挂起时把身份证留在原地,恢复时已在别处,自然无法通过闸机。
📌 最佳实践清单
- 协程内同步 → 无脑选
Mutex
(来自kotlinx-coroutines-core,轻量且安全) - 与 Java 代码交互时:
- 确保加锁/解锁在无挂起点的连续代码块中
- 或将同步逻辑封装为 suspend 函数,内部用 Mutex
- 警惕“看似安全”的写法:
// 危险!lock() 和 unlock() 被挂起点隔开 lock.lock() try { doSomething() // 可能含挂起点! } finally { lock.unlock() // 恢复时线程已变,必崩! } - 工具推荐:
- 使用
kotlinx-coroutines的Semaphore、Channel处理更复杂同步 - 静态检查:开启 Kotlin 编译器警告(检测 synchronized 内挂起点)
- 使用
🌟 结语
“Java 的锁锁不住 Kotlin 协程”——
这句话本身是个美丽的误会。
锁从未失效,只是用错了战场。
真正的关键在于:
🔹 理解工具的设计边界
🔹 让同步机制匹配编程模型
🔹 尊重并发范式的演进逻辑
当你下次写 synchronized 时,不妨自问:
“此刻,我锁住的究竟是线程,还是逻辑?”
技术没有银弹,但有敬畏之心。
愿你的协程,永远不被“锁”困住。
📌 延伸阅读
- 《Kotlin Coroutines by Tutorials》Chapter 7: Thread Safety
- kotlinx.coroutines 官方文档:Shared Mutable State
- 源码深挖:
MutexImpl.kt如何实现协程级锁
💬 互动话题
你在协程同步中踩过哪些坑?欢迎留言分享~
(点赞+在看,技术不迷路✨)
