Scala 与 JVM:互操作、取消和性能实验
前置:集合、Either与错误。本页在JDK21与Scala3.10.0上运行,不涉及Spark集群。
Java互操作与资源作用域
Scala可调用Java类库,但Java的null、异常与线程协议仍然存在。Files.newBufferedReader返回Reader;readLine到末尾返回null,所以我们在边界用takeWhile(_ != null)转成有限Iterator。不能因为Scala有Option就假定Java API自动返回Option。外部可空引用应在边界转换或校验,启用显式空值检查时还需按对应模式调整互操作声明。
Using.resource(reader) { ... }在正常返回或异常时关闭AutoCloseable。把依赖reader的Iterator返回到using外面会在真正消费时读已关闭文件;应在作用域内消费并返回独立结果。库若接受借用Iterator不会自动知道它背后是否有文件,所有者必须保证其生存期覆盖消费。本项目main创建并关闭Reader,summarize只消费迭代器,职责明确。
case class和Scala集合不能自动当JavaBeans/Java集合;使用集合转换器建立Java视图时要注意它可能共享底层对象,复制与包装不是同一件事。Scala字符串在JVM上是java.lang.String,编码、代码单元和平台文件路径规则继续适用。大小写排序与业务键规范应显式约定,不能依赖运行机区域设置。
Future、ExecutionContext与取消
Scala标准库Future通常代表已安排到ExecutionContext的计算,没有通用cancel。Await超时只是停止等待,不能杀掉工作;全局ExecutionContext不适合无限阻塞文件操作。对阻塞任务可用明确拥有的ExecutorService,并在退出时shutdown/awaitTermination,限制线程数量。CPU密集任务适合按核数限制并行,I/O任务的线程数应按延迟与外部容量推导,不能一概“越多越快”。
Java Future.cancel(true)请求中断正在执行的线程;这也是协作协议。Thread.sleep、等待锁等可中断方法会抛InterruptedException;CPU循环若从不检查interrupted状态则可能继续运行。取消Future会使get抛CancellationException,不等于底层已经完成清理。因此Lab额外等待stopped锁存器,再关闭线程池并确认终止。不要把“Future状态cancelled”当“资源已释放”的唯一证据。
Actor并不是语言内建必然存在的通用并发模型;Akka/Pekko等是有版本、许可与运行语义的独立生态。旧简介中“Scala内置Actor和并发集合”不足以指导现代Scala3工程,已在新课程纠正;本批不拿依赖缺失的Actor片段充当可运行并发项目。
实际可运行的中断、given与集合实验
Lab.scala具备所有import、线程池关闭和显式断言。它先以CountDownLatch确认任务开始,再cancel(true),测试get失败与finally完成,最后保证线程池退出。随后比较一次性Iterator求和与先物化Vector求和。
Lab.scala
import java.util.concurrent.{Callable, CountDownLatch, Executors, TimeUnit, CancellationException}
trait Show[A]:
def show(value: A): String
object Lab:
given Show[Int] with
def show(value: Int): String = s"int:$value"
def render[A](value: A)(using show: Show[A]): String = show.show(value)
def main(args: Array[String]): Unit =
assert(render(3) == "int:3")
val pool = Executors.newSingleThreadExecutor()
val started = new CountDownLatch(1)
val stopped = new CountDownLatch(1)
try
val future = pool.submit(new Callable[Int]:
def call(): Int =
started.countDown()
try
Thread.sleep(30000)
1
finally stopped.countDown()
)
assert(started.await(5, TimeUnit.SECONDS))
assert(future.cancel(true))
try
future.get()
throw new AssertionError("cancellation not observed")
catch case _: CancellationException => ()
assert(stopped.await(5, TimeUnit.SECONDS))
finally
pool.shutdownNow()
assert(pool.awaitTermination(5, TimeUnit.SECONDS))
for materialize <- List(false, true) do
val start = System.nanoTime()
val values = (0 until 100000).iterator.filter(_ % 2 == 0).map(n => n.toLong * n)
val sum = if materialize then values.toVector.sum else values.sum
assert(sum == 166661666700000L)
println(s"materialize=$materialize sum=$sum ns=${System.nanoTime() - start}")
println("given instance and cooperative JVM interruption passed")编译scalac -d out Lab.scala,按起步页类路径运行Lab。预期两次sum=166661666700000以及given instance and cooperative JVM interruption passed;纳秒数不固定。断言失败非零退出,取消/线程池等待有5秒测试看门狗,不把睡眠时长当调度保证。
JVM有JIT、类加载、GC与逃逸分析,单轮ns数据不是稳定基准;先预热、多次交替、校验相同输入/输出,再考虑JMH等专业工具。当前实验只证明两条实际路径结果一致与能观察成本,不宣称Iterator一定更快。Scala的装箱、函数对象与集合构建都会影响分配,语法简洁不等于零开销。
诊断时java -Xlog:gc* ...打印真实GC事件;jcmd -l列出可见JVM进程,进程仍活跃且权限满足时用jcmd PID Thread.print检查等待点。JFR可用java -XX:StartFlightRecording=filename=lab.jfr,dumponexit=true ...采样,运行后jfr summary lab.jfr确认文件有效,再查看热点。短命令可能几乎无样本,不能因此断定没有热点。jcmd、javap、JFR是JDK工具,缺命令先确认使用完整JDK而非只有JRE。
自检与参考方向
练习:将Lab的sleep改为不检查中断的死循环,预测哪项断言会失败;为什么不能把该反例加进普通测试无条件执行?将summary Iterator移出Using后何时出错?性能比较先物化与后物化分别影响什么?
参考解答
Future可进入取消状态,但stopped不会完成,线程池不能在期限内终止;无限任务会拖住进程,应在独立受控进程/外部看门狗运行,不能污染正常测试。惰性Iterator离开资源作用域后才访问Reader会失败。物化增加容器分配、固定当时数据并支持反复遍历;Iterator只可单次消费。实验需保持语义一致,不把不同工作量时间作比较。
依据:Scala Future、Using API、Java Future取消、JDK Flight Recorder。
继续阅读
- 开发实践
- 类型、表达式与建模
- 类型、表达式与建模