You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 20:55:17