bigywater.tech
Java · 185

ScopedValue:为什么它比 ThreadLocal 更适合上下文传递

ScopedValue 为什么被说成比 ThreadLocal 更适合传递上下文?这篇用 JDK 27(ScopedValue 已正式)逐项验证:范围、不可变、线程池泄漏、线程间传递,并如实检验「更省内存」这个说法(虚拟线程的基本行为见前面的篇目)。

NoSuchElementException
绑定范围外调用 get()
泄漏 vs 无泄漏
线程池里 ThreadLocal 与 ScopedValue 跨任务的表现
97 vs 95 MB
10 万个虚拟线程用 ThreadLocal 与 ScopedValue 的堆增加(几乎一样)

绑定范围

实测 · 本机执行输出
  绑定之前:USER.isBound() = false
  绑定之前 USER.get() → NoSuchElementException
  绑定范围内:USER.get() = ann,isBound = true
  嵌套重新绑定:内层 USER.get() = bob
  内层结束后外层 USER.get() = ann(自动恢复)
  orElse:USER.orElse("默认") = ann
  范围结束后:USER.isBound() = false
  call 的返回值:carol
  范围里抛异常传出来之后:USER.isBound() = false(异常也会解除绑定)
  ScopedValue 只有 get / isBound / orElse / orElseThrow,没有 set 方法:[]

线程池泄漏

实测 · 本机执行输出
  单线程池里,任务 1 对 ThreadLocal 调了 set 但没有 remove;任务 2 读到:上一个任务设置的值(泄漏到下一个任务)
  任务 1 用 ScopedValue 绑定;任务 2 读到:(未绑定)(没有泄漏)

新线程

实测 · 本机执行输出
  main 里设置了 ThreadLocal、InheritableThreadLocal,并在 ScopedValue 绑定范围内启动新线程:
    平台线程里读到:TL=null,InheritableTL=itl-main,ScopedValue=(未绑定)
    虚拟线程里读到:TL=null,InheritableTL=itl-main,ScopedValue=(未绑定)
  (ScopedValue 不会自动传给用 new Thread 启动的线程;要传,需要在新线程里重新绑定,或用结构化并发(preview,本篇没有验证))

内存

实测 · 本机执行输出
  100000 个同时存活的虚拟线程(每个都阻塞在同一个 latch 上,所有线程用的都是同一个共享的 1 KB 数组),堆增加:什么都不绑定 99 MB;每个线程 ThreadLocal.set(共享对象) 97 MB;每个线程 ScopedValue.where(共享对象).run 95 MB(各 1047 / 1021 / 1001 字节每线程)

分步演示

ScopedValue
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 27(ScopedValue 在此版本是正式 API,无需 --enable-preview;JDK 21 上用它会编译失败:ScopedValue is a preview API and is disabled by default)
    • 没有验证:StructuredTaskScope 与 ScopedValue 的配合(预览特性)、get() 的耗时与 ThreadLocal.get() 的对比、虚拟线程以外的平台线程场景的内存、JDK 21~24 预览阶段的 API 差异;内存对比只用了每线程 1 KB 的共享数据一种场景
    京ICP备2026067502号-1京公网安备11010502063487号