作者:悬衡
一、前言
在 Java 中有一个非常经典的死锁问题, 就是明明自己已经占用了线程池, 却还继续去申请它, 自己等自己, 就死锁了, 如下图和代码:
// 这段代码将死锁到天荒地老final ExecutorService executorService = Executors.newSingleThreadExecutor();executorService.submit(() -> {Future<?> subTask = executorService.submit(() -> System.out.println("Hello dead lock"));try {subTask.get();} catch (ExecutionException | InterruptedException ignore) { }});
相比别的死锁问题, 这一类问题的坑点在于, 因为线程池的实现问题, jstack 等 jvm 工具无法对其自动诊断, 只能肉眼看出。
在 Kotlin 协程中, 因为底层的线程池申请更加黑盒, 如果不是足够了解, 很容易踩到这类坑。
本文不会再去重复 Kotlin 协程的基本语法, 而是专注于死锁的话题。
下面两段代码你觉得是否有死锁风险?:
-
第一段代码看起来很恶心, 但是它反而是没有死锁风险的
runBlocking(Dispatchers.IO) {runBlocking {launch (Dispatchers.IO) {println("hello coroutine")}}}
-
第二段代码看着 "挺简洁的", 其实是有死锁风险的
runBlocking(Dispatchers.IO) {runBlocking {launch (Dispatchers.IO) {println("hello coroutine")}}}
只要同一时间有 64 个请求同时进入这个代码块, 就永远不要想出来了, 而且因为协程的线程池都是复用的, 其他协程也别想执行了, 比如下面这段代码就能锁死整个应用:
// 用传统 Java 线程池来模拟 64 个请求val threadPool = Executors.newFixedThreadPool(64)repeat(64) {threadPool.submit {runBlocking(Dispatchers.IO) {println("hello runBlocking $it")// 在协程环境中本不应该调用 sleep, 这里为了模拟耗时计算和调用,不得已使用// 正常协程休眠应该用 delayThread.sleep(5000)runBlocking {launch (Dispatchers.IO) {// 因为死锁, 下面这行永远都打印不出来println("hello launch $it")}}}}}Thread.sleep(5000)runBlocking(Dispatchers.IO) {// 别的协程也执行不了, 下面这行也永远打印不出来println("hello runBlocking2")}
随便翻翻代码仓库, 就能看到大量存在类似风险的代码, 之前还差点因此发生事故。
本文将会剖析 Kotlin 协程死锁的根本原因, 以及如何彻底地从坑中爬出来。
笔者主要是做服务端的, 文中内容可能更贴近服务端开发场景, 如果移动端场景有所不同, 也欢迎在评论区讨论。
二、runBlocking 线程调度常识
2.1 主线程的独角戏
runBlocking 从表面上理解就是开启一个协程, 并且等待它结束。
Java 的线程思维总让人觉得 runBlocking 会用一个新线程异步执行其中的代码块, 实际上不是这样。runBlocking 在不加参数时, 默认使用当前线程执行:
fun main() {println("External Thread name: ${Thread.currentThread().name}")runBlocking {println("Inner Thread name: ${Thread.currentThread().name}")}}
输出如下:
External Thread name: mainInner Thread name: main
如果我

最低0.47元/天 解锁文章
1163

被折叠的 条评论
为什么被折叠?



