Scala 3.2中线程引用外部变量引发死锁的原因探究
Scala 3.2线程死锁问题分析
问题复现
以下Scala 3.2代码尝试启动两个线程打印任务信息和参数,但运行后仅输出线程启动信息,无法输出"Done!"且程序无法终止:
object ThreadingDeadlock extends App { val jobs = List("foo", "bar") val parameter = "Param" val threads = jobs .map(job => { new Thread(() => { println(s"Thread started for job $job") println(s"Parameter: $parameter") }) }) threads.foreach(t => t.start()) threads.foreach(t => t.join()) println("Done!") }
但注释掉打印parameter的语句,或者在map内将parameter赋值给局部变量param后,程序能正常运行并输出完整内容。
原因分析
1. Scala App特质的初始化逻辑
Scala的App特质基于DelayedInit实现,它会把对象体内的执行代码(除成员变量初始化外)延迟到一个特殊的初始化块中运行,这个初始化过程受JVM的类初始化锁保护。主线程执行App对象的初始化代码时,会持有该锁,直到整个初始化流程完成。
2. 死锁的形成过程
线程代码中直接引用parameter(App对象的成员变量)时,JVM会要求ThreadingDeadlock对象必须完成初始化才能访问其成员。此时新启动的子线程会尝试获取ThreadingDeadlock的类初始化锁,但该锁正被主线程持有——主线程此时卡在threads.foreach(t => t.join())步骤,等待子线程执行完毕。
这就形成了死锁循环:
- 主线程:持有类初始化锁 → 等待子线程终止
- 子线程:等待获取类初始化锁 → 无法执行打印
parameter的逻辑,也无法结束
3. 修复方案的原理
- 注释打印
parameter的语句:子线程不再访问App对象的成员,无需等待类初始化锁,能顺利执行完并终止,主线程join后继续执行后续代码。 - 将
parameter赋值给局部变量:局部变量会被捕获到线程闭包中,子线程访问的是局部变量的副本,不依赖App对象的初始化状态,也就不会触发对类初始化锁的等待,线程能正常执行完毕。
相关JVM隐式机制
- 类初始化锁:JVM为每个类/对象分配一个初始化锁,确保类的初始化过程线程安全,同一时间仅一个线程能执行类初始化逻辑。其他线程尝试访问未完成初始化的类/对象成员时,会阻塞等待锁释放。
- 闭包变量捕获:Scala闭包会捕获外部变量,若捕获对象成员变量,会保留对对象的引用;若捕获局部变量,则会复制变量值到闭包内部(不可变值类型直接复制,引用类型复制引用)。
内容的提问来源于stack exchange,提问作者Bennik2000
相关产品推荐
相关产品推荐

