Kotlin对象初始化时结合withContext与日志操作引发死锁的原因排查
Kotlin对象初始化时结合withContext与日志操作引发死锁的原因排查
这问题挺典型的,咱们来一步步拆解死锁发生的核心逻辑:
先还原你的场景
你定义了一个Kotlin单例object,在初始化块里用runBlocking启动协程:
package com.example import org.slf4j.LoggerFactory import java.lang.Thread.currentThread import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext object Object { private val log = LoggerFactory.getLogger(Object::class.java) init { runBlocking { log.info("[${currentThread().name}] Object initializing") withContext(Dispatchers.IO) { log.info("[${currentThread().name}] Object initialized") } } } fun f() = Unit }
当外部调用Object.f()触发初始化时,只输出第一条日志,程序卡在withContext块里,栈trace显示IO线程的协程在等待com.example.Object的类初始化监视器锁。
死锁的核心原因:类初始化锁+协程阻塞的循环依赖
Kotlin的object是JVM层面线程安全的懒加载单例,JVM在初始化类/对象时,会自动持有一个类初始化监视器锁——只有当整个初始化流程(属性赋值+init块执行)完全完成后,才会释放这个锁。
咱们把执行流程拆成两个线程的互相等待:
- 主线程流程:调用
Object.f()触发初始化 → 持有Object的类初始化锁 → 进入runBlocking→ 执行完第一条日志后,等待withContext(Dispatchers.IO)里的协程执行完毕才会继续,进而完成初始化、释放锁。 - IO线程协程流程:切换到IO线程后执行
log.info→ SLF4J的日志实现(比如Logback)在打印日志时,会尝试获取类的元信息(比如类全名、调用栈上下文),这个过程中会触发JVM对com.example.Object类的初始化状态检查 → 尝试获取已经被主线程持有的类初始化锁。
这就形成了死锁闭环:主线程拿着锁等IO协程完成,IO协程等着锁才能完成日志输出,两边都卡着动不了。
为什么换println或GlobalScope.launch就没事?
- 用println替代日志:println只是简单输出字符串,不会触发任何需要访问
Object类元信息的操作,IO协程能顺利执行完,主线程拿到结果后完成初始化、释放锁,流程正常。 - 用GlobalScope.launch:这个协程是后台异步启动的,
runBlocking不会等待它执行完毕,所以init块很快就执行完成,主线程释放类初始化锁;之后后台协程再执行日志输出时,锁已经释放了,自然不会卡住。
总结
本质是JVM类初始化的锁机制,和runBlocking的阻塞等待特性叠加,形成了循环依赖。要避免这种情况,尽量不要在类/object的初始化块里用阻塞式协程调用涉及自身类元信息的操作,或者改用非阻塞的异步初始化方式。
内容来源于stack exchange
相关产品推荐
相关产品推荐

