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

Scala中Future风格并发编程为何需要final关键字?

Future并发编程中final关键字导致程序阻塞的问题解析

问题描述

在Future风格并发编程中遇到了一个与final关键字相关的异常:编写的程序会生成多个Future,打印指定数值后退出。当Future代码块中使用的常量以final val声明时,程序运行符合预期;但省略final修饰符后,程序会无限阻塞,无法正常终止。

程序代码

object Futures extends App {
  final val TICK_MS = 10
  final val N_TICKS = 300
  
  def doPrint(num: Int, p: Promise[String]): Future[Int] = {
    val f = Future {
      blocking { Thread.sleep(TICK_MS) }
      if num > N_TICKS then p.success("Futures done.")
      num + 1
    }
    if num % 10 == 0 then println(num)
    for v <- f do doPrint(v, p)
    f
  }

  val p = Promise[String]()
  doPrint(0, p)

  p.future.onComplete {
    case Success(v) => println(v)
    case Failure(e) => e.printStackTrace()
  }

  Await.ready(p.future, Duration.Inf)
}

预期与实际表现

  • 预期输出:依次打印10、20……300,最后输出“Futures done”并退出程序
  • 异常表现:将final val替换为普通val后,程序无限阻塞,永远不会触发Promise的success逻辑

运行环境

  • sbt.version: 1.8.2
  • scalaVersion: 3.2.2
  • JVM: zulu19.32.15
  • macOS Venture 13.3,Apple M1,16GB

原因解析

这个问题的核心是Scala中val与final val的语义差异,以及JVM并发模型下的可见性规则:

  1. final val的编译期特性
    final val属于编译期常量,编译器会执行常量折叠优化——直接将代码中所有引用该常量的位置替换为它的字面量值。在你的代码里,num > N_TICKS会被替换为num > 300,Future线程执行时能准确判断终止条件,触发p.success结束程序。

  2. 普通val的并发可见性问题
    普通val虽然不可变,但不属于编译期常量,编译器不会做常量折叠。在并发场景下,Future运行在独立线程中,JVM无法保证普通val的跨线程可见性(没有同步机制或final修饰的情况下)。同时,JIT编译器可能会对普通val的getter方法进行激进优化,导致Future线程读取到的N_TICKS值始终无法满足num > N_TICKS的条件,递归调用永远不会终止,Promise也永远不会被完成,最终程序无限阻塞。

  3. 核心结论
    final val既保证了值的编译期常量性,又通过JVM的final语义确保了跨线程的可见性,让Future线程能准确执行终止逻辑;普通val缺少这些保障,在并发场景下因可见性或编译器优化问题,导致程序无法正常结束。

内容的提问来源于stack exchange,提问作者Ken

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 22:17:30