ThreadLocal 在虚拟线程间要小心

一、前言

虚拟线程(Virtual Thread)是 Java 21 最重磅的特性之一,它让开发者可以轻松创建数百万个轻量级线程,彻底改变了高并发应用的编程模型。然而,很多开发者在迁移到虚拟线程后发现:原本在平台线程下运行良好的 ThreadLocal,在虚拟线程场景下不仅没带来性能提升,反而成了内存泄漏和上下文丢失的罪魁祸首。

为此,Java 官方在 JDK 20 就开始孵化一个全新的 API------ScopedValue(作用域值) ,并在 JDK 21 中作为预览特性(JEP 446)正式推出,最终在 JDK 25 中通过 JEP 506 转正。

📌 核心结论先行 :ThreadLocal 的设计前提是线程池化 + 长期存活 + 少量线程 ,而虚拟线程不池化、短生命周期、数量可达百万级 ,两者根本矛盾。ScopedValue 专为虚拟线程设计,不可变、作用域受限、自动清理,是应用级请求上下文传递的正确选择。

二、ThreadLocal 的设计前提

要理解为什么 ThreadLocal 在虚拟线程下会出问题,首先需要回顾它为什么在平台线程下工作得那么好。

2.1 ThreadLocal 的经典用法

ThreadLocal 的典型模式非常简单:声明一个 static 的 ThreadLocal<T> 字段,JVM 保证每个调用 get() 或 set() 的线程都看到自己的私有副本,无需同步,无竞争,每个线程拥有完全隔离的状态。

2.2 底层结构

每个 Thread 实例内部有一个 threadLocals 字段,指向一个 ThreadLocalMap------一个为小数据量优化的自定义哈希表。ThreadLocal 对象本身作为 key,存储的值作为 map entry 的 value。

关键细节:key 通过 WeakReference 持有 ,如果 ThreadLocal key 被 GC 回收,对应的槽位最终可以被清理。但 value 是强引用,只要线程存活,value 就存活。
#mermaid-svg-R6LTRAp1xGSK8XDN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-R6LTRAp1xGSK8XDN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-R6LTRAp1xGSK8XDN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-R6LTRAp1xGSK8XDN .error-icon{fill:#552222;}#mermaid-svg-R6LTRAp1xGSK8XDN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-R6LTRAp1xGSK8XDN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-R6LTRAp1xGSK8XDN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-R6LTRAp1xGSK8XDN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-R6LTRAp1xGSK8XDN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-R6LTRAp1xGSK8XDN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-R6LTRAp1xGSK8XDN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-R6LTRAp1xGSK8XDN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-R6LTRAp1xGSK8XDN .marker.cross{stroke:#333333;}#mermaid-svg-R6LTRAp1xGSK8XDN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-R6LTRAp1xGSK8XDN p{margin:0;}#mermaid-svg-R6LTRAp1xGSK8XDN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-R6LTRAp1xGSK8XDN .cluster-label text{fill:#333;}#mermaid-svg-R6LTRAp1xGSK8XDN .cluster-label span{color:#333;}#mermaid-svg-R6LTRAp1xGSK8XDN .cluster-label span p{background-color:transparent;}#mermaid-svg-R6LTRAp1xGSK8XDN .label text,#mermaid-svg-R6LTRAp1xGSK8XDN span{fill:#333;color:#333;}#mermaid-svg-R6LTRAp1xGSK8XDN .node rect,#mermaid-svg-R6LTRAp1xGSK8XDN .node circle,#mermaid-svg-R6LTRAp1xGSK8XDN .node ellipse,#mermaid-svg-R6LTRAp1xGSK8XDN .node polygon,#mermaid-svg-R6LTRAp1xGSK8XDN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-R6LTRAp1xGSK8XDN .rough-node .label text,#mermaid-svg-R6LTRAp1xGSK8XDN .node .label text,#mermaid-svg-R6LTRAp1xGSK8XDN .image-shape .label,#mermaid-svg-R6LTRAp1xGSK8XDN .icon-shape .label{text-anchor:middle;}#mermaid-svg-R6LTRAp1xGSK8XDN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-R6LTRAp1xGSK8XDN .rough-node .label,#mermaid-svg-R6LTRAp1xGSK8XDN .node .label,#mermaid-svg-R6LTRAp1xGSK8XDN .image-shape .label,#mermaid-svg-R6LTRAp1xGSK8XDN .icon-shape .label{text-align:center;}#mermaid-svg-R6LTRAp1xGSK8XDN .node.clickable{cursor:pointer;}#mermaid-svg-R6LTRAp1xGSK8XDN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-R6LTRAp1xGSK8XDN .arrowheadPath{fill:#333333;}#mermaid-svg-R6LTRAp1xGSK8XDN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-R6LTRAp1xGSK8XDN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-R6LTRAp1xGSK8XDN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-R6LTRAp1xGSK8XDN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-R6LTRAp1xGSK8XDN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-R6LTRAp1xGSK8XDN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-R6LTRAp1xGSK8XDN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-R6LTRAp1xGSK8XDN .cluster text{fill:#333;}#mermaid-svg-R6LTRAp1xGSK8XDN .cluster span{color:#333;}#mermaid-svg-R6LTRAp1xGSK8XDN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-R6LTRAp1xGSK8XDN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-R6LTRAp1xGSK8XDN rect.text{fill:none;stroke-width:0;}#mermaid-svg-R6LTRAp1xGSK8XDN .icon-shape,#mermaid-svg-R6LTRAp1xGSK8XDN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-R6LTRAp1xGSK8XDN .icon-shape p,#mermaid-svg-R6LTRAp1xGSK8XDN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-R6LTRAp1xGSK8XDN .icon-shape .label rect,#mermaid-svg-R6LTRAp1xGSK8XDN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-R6LTRAp1xGSK8XDN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-R6LTRAp1xGSK8XDN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-R6LTRAp1xGSK8XDN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ThreadLocal 底层结构
Thread 实例
threadLocals

ThreadLocalMap
Entry
Entry
Key: WeakReference

指向 ThreadLocal
Value: StrongReference

强引用,线程存活则存活

2.3 核心假设:线程池化 + 长期存活

ThreadLocal 的缓存模式之所以高效,建立在两个关键假设之上:

  1. 线程是长期存活的(应用启动时创建,运行期间一直存在)
  2. 线程被大量复用(一个线程处理成千上万个请求)

在这种情况下,一个昂贵的对象(如 SimpleDateFormat、ObjectMapper)只需要创建 200 次(对应 200 个平台线程),后续所有请求都能复用。创建成本被均摊到每个线程处理的数千个请求上,收益非常可观。

三、虚拟线程如何颠覆 ThreadLocal 的假设

3.1 虚拟线程的核心特征

JEP 444 明确指出:虚拟线程不应该被池化,因为每个虚拟线程只用于运行一个任务。虚拟线程的生命周期就是:为任务而创建 → 运行至完成 → 被丢弃。没有复用,没有线程池。

更进一步,虚拟线程足够轻量,应用可以同时运行数十万个。一个处理 5 万并发请求的服务在使用虚拟线程时并不罕见。
#mermaid-svg-Lbe4xLjJVxy3q8fH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Lbe4xLjJVxy3q8fH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Lbe4xLjJVxy3q8fH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Lbe4xLjJVxy3q8fH .error-icon{fill:#552222;}#mermaid-svg-Lbe4xLjJVxy3q8fH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Lbe4xLjJVxy3q8fH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Lbe4xLjJVxy3q8fH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Lbe4xLjJVxy3q8fH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Lbe4xLjJVxy3q8fH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Lbe4xLjJVxy3q8fH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Lbe4xLjJVxy3q8fH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Lbe4xLjJVxy3q8fH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Lbe4xLjJVxy3q8fH .marker.cross{stroke:#333333;}#mermaid-svg-Lbe4xLjJVxy3q8fH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Lbe4xLjJVxy3q8fH p{margin:0;}#mermaid-svg-Lbe4xLjJVxy3q8fH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Lbe4xLjJVxy3q8fH .cluster-label text{fill:#333;}#mermaid-svg-Lbe4xLjJVxy3q8fH .cluster-label span{color:#333;}#mermaid-svg-Lbe4xLjJVxy3q8fH .cluster-label span p{background-color:transparent;}#mermaid-svg-Lbe4xLjJVxy3q8fH .label text,#mermaid-svg-Lbe4xLjJVxy3q8fH span{fill:#333;color:#333;}#mermaid-svg-Lbe4xLjJVxy3q8fH .node rect,#mermaid-svg-Lbe4xLjJVxy3q8fH .node circle,#mermaid-svg-Lbe4xLjJVxy3q8fH .node ellipse,#mermaid-svg-Lbe4xLjJVxy3q8fH .node polygon,#mermaid-svg-Lbe4xLjJVxy3q8fH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Lbe4xLjJVxy3q8fH .rough-node .label text,#mermaid-svg-Lbe4xLjJVxy3q8fH .node .label text,#mermaid-svg-Lbe4xLjJVxy3q8fH .image-shape .label,#mermaid-svg-Lbe4xLjJVxy3q8fH .icon-shape .label{text-anchor:middle;}#mermaid-svg-Lbe4xLjJVxy3q8fH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Lbe4xLjJVxy3q8fH .rough-node .label,#mermaid-svg-Lbe4xLjJVxy3q8fH .node .label,#mermaid-svg-Lbe4xLjJVxy3q8fH .image-shape .label,#mermaid-svg-Lbe4xLjJVxy3q8fH .icon-shape .label{text-align:center;}#mermaid-svg-Lbe4xLjJVxy3q8fH .node.clickable{cursor:pointer;}#mermaid-svg-Lbe4xLjJVxy3q8fH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Lbe4xLjJVxy3q8fH .arrowheadPath{fill:#333333;}#mermaid-svg-Lbe4xLjJVxy3q8fH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Lbe4xLjJVxy3q8fH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Lbe4xLjJVxy3q8fH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Lbe4xLjJVxy3q8fH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Lbe4xLjJVxy3q8fH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Lbe4xLjJVxy3q8fH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Lbe4xLjJVxy3q8fH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Lbe4xLjJVxy3q8fH .cluster text{fill:#333;}#mermaid-svg-Lbe4xLjJVxy3q8fH .cluster span{color:#333;}#mermaid-svg-Lbe4xLjJVxy3q8fH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Lbe4xLjJVxy3q8fH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Lbe4xLjJVxy3q8fH rect.text{fill:none;stroke-width:0;}#mermaid-svg-Lbe4xLjJVxy3q8fH .icon-shape,#mermaid-svg-Lbe4xLjJVxy3q8fH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Lbe4xLjJVxy3q8fH .icon-shape p,#mermaid-svg-Lbe4xLjJVxy3q8fH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Lbe4xLjJVxy3q8fH .icon-shape .label rect,#mermaid-svg-Lbe4xLjJVxy3q8fH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Lbe4xLjJVxy3q8fH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Lbe4xLjJVxy3q8fH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Lbe4xLjJVxy3q8fH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 虚拟线程模型
每个请求: 1 个新虚拟线程
线程运行完即销毁
ThreadLocal 每次重新创建

❌ 浪费内存
平台线程模型
线程池: 200 个平台线程
线程长期存活
ThreadLocal 缓存被复用

✅ 高效

3.2 内存泄漏的量化分析

社区实验数据非常直观:使用 ThreadLocal 创建 10 万个虚拟线程,内存增加约 248MB (从 6MB 到 254MB),平均每个虚拟线程的 ThreadLocal 开销约为 2.5KB。

如果扩展到百万级虚拟线程,可能导致 GB 级内存消耗。

原因在于:每个虚拟线程都会创建独立的 ThreadLocalMap,而虚拟线程的数量是平台线程的成千上万倍,导致内存占用呈指数级增长。

3.3 更隐蔽的问题:上下文丢失与内存残留

除了内存膨胀,ThreadLocal 在虚拟线程下还有两个更隐蔽的陷阱:

上下文丢失 :虚拟线程可以在不同的载体线程之间调度。当虚拟线程被卸载并重新挂载到另一个载体线程上时,如果代码依赖 ThreadLocal 存储上下文,可能导致上下文指向错误的数据,甚至引发崩溃或 native 堆损坏。

内存残留 :当虚拟线程结束时,它在 ThreadLocal 中的数据可能仍然滞留在内存中------即使线程本身早已消亡。这会导致泄漏,因为 JVM 并不总是知道这些值已经不再需要了。
#mermaid-svg-yMS1VnEE01JQUxN1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yMS1VnEE01JQUxN1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yMS1VnEE01JQUxN1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yMS1VnEE01JQUxN1 .error-icon{fill:#552222;}#mermaid-svg-yMS1VnEE01JQUxN1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yMS1VnEE01JQUxN1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yMS1VnEE01JQUxN1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yMS1VnEE01JQUxN1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yMS1VnEE01JQUxN1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yMS1VnEE01JQUxN1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yMS1VnEE01JQUxN1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yMS1VnEE01JQUxN1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yMS1VnEE01JQUxN1 .marker.cross{stroke:#333333;}#mermaid-svg-yMS1VnEE01JQUxN1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yMS1VnEE01JQUxN1 p{margin:0;}#mermaid-svg-yMS1VnEE01JQUxN1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yMS1VnEE01JQUxN1 .cluster-label text{fill:#333;}#mermaid-svg-yMS1VnEE01JQUxN1 .cluster-label span{color:#333;}#mermaid-svg-yMS1VnEE01JQUxN1 .cluster-label span p{background-color:transparent;}#mermaid-svg-yMS1VnEE01JQUxN1 .label text,#mermaid-svg-yMS1VnEE01JQUxN1 span{fill:#333;color:#333;}#mermaid-svg-yMS1VnEE01JQUxN1 .node rect,#mermaid-svg-yMS1VnEE01JQUxN1 .node circle,#mermaid-svg-yMS1VnEE01JQUxN1 .node ellipse,#mermaid-svg-yMS1VnEE01JQUxN1 .node polygon,#mermaid-svg-yMS1VnEE01JQUxN1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yMS1VnEE01JQUxN1 .rough-node .label text,#mermaid-svg-yMS1VnEE01JQUxN1 .node .label text,#mermaid-svg-yMS1VnEE01JQUxN1 .image-shape .label,#mermaid-svg-yMS1VnEE01JQUxN1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-yMS1VnEE01JQUxN1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yMS1VnEE01JQUxN1 .rough-node .label,#mermaid-svg-yMS1VnEE01JQUxN1 .node .label,#mermaid-svg-yMS1VnEE01JQUxN1 .image-shape .label,#mermaid-svg-yMS1VnEE01JQUxN1 .icon-shape .label{text-align:center;}#mermaid-svg-yMS1VnEE01JQUxN1 .node.clickable{cursor:pointer;}#mermaid-svg-yMS1VnEE01JQUxN1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yMS1VnEE01JQUxN1 .arrowheadPath{fill:#333333;}#mermaid-svg-yMS1VnEE01JQUxN1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yMS1VnEE01JQUxN1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yMS1VnEE01JQUxN1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yMS1VnEE01JQUxN1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yMS1VnEE01JQUxN1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yMS1VnEE01JQUxN1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yMS1VnEE01JQUxN1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yMS1VnEE01JQUxN1 .cluster text{fill:#333;}#mermaid-svg-yMS1VnEE01JQUxN1 .cluster span{color:#333;}#mermaid-svg-yMS1VnEE01JQUxN1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yMS1VnEE01JQUxN1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yMS1VnEE01JQUxN1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-yMS1VnEE01JQUxN1 .icon-shape,#mermaid-svg-yMS1VnEE01JQUxN1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yMS1VnEE01JQUxN1 .icon-shape p,#mermaid-svg-yMS1VnEE01JQUxN1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yMS1VnEE01JQUxN1 .icon-shape .label rect,#mermaid-svg-yMS1VnEE01JQUxN1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yMS1VnEE01JQUxN1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yMS1VnEE01JQUxN1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yMS1VnEE01JQUxN1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
虚拟线程开始
ThreadLocal.set 存储上下文
虚拟线程被卸载
调度到另一个载体线程
ThreadLocal 是否绑定到载体线程?
❌ 上下文丢失/指向错误数据
✅ 上下文正确
虚拟线程结束
ThreadLocal value 残留内存
❌ GC 无法回收,内存泄漏

3.4 第三方库的连锁反应

问题不仅限于应用代码。许多第三方库(如日志框架、数据库连接池、JSON 序列化库)内部使用 ThreadLocal 作为缓存或池化机制。

当这些库在虚拟线程环境下运行时,每个虚拟线程都会创建一份新的缓存副本,导致大量短命的 8KB 分配 ,因为 ThreadLocal 缓存在线程短暂存在时无法被复用。

四、ScopedValue:专为虚拟线程设计的上下文传递原语

4.1 什么是 ScopedValue

ScopedValue(作用域值)是 JDK 20 引入(JEP 429),JDK 21 转为预览(JEP 446),JDK 25 正式转正(JEP 506)的 API。

JEP 429 对它的定义是:引入作用域值,它允许在线程内和线程间共享不可变数据。它们优于线程局部变量,尤其是在使用大量虚拟线程时。

本质上,ScopedValue 就是一个隐式方法参数------它"好像"调用链中的每个方法都有一个额外的、不可见的参数,但没有任何方法声明这个参数,只有能够访问 ScopedValue 对象的方法才能获取其值。

4.2 ScopedValue 的核心设计目标

JEP 429 定义了四个目标:

目标 说明
易用性 提供编程模型,在线程内和子线程间共享数据,简化数据流推理
可理解性 让共享数据的生命周期从代码的语法结构中可见
健壮性 确保调用者共享的数据只能被合法的被调用者获取
性能 将共享数据视为不可变,允许大量线程共享,并支持运行时优化

4.3 核心 API

ScopedValue 的 API 非常简洁:

java 复制代码
// 1. 声明一个 ScopedValue(必须是 static final)
public static final ScopedValue<RequestContext> CONTEXT = ScopedValue.newInstance();

// 2. 绑定值并执行操作(where().run() 或 where().call())
ScopedValue.where(CONTEXT, requestContext).run(() -> {
    // 在这个作用域内,CONTEXT.get() 返回 requestContext
    Application.handle(request, response);
});
// 作用域结束后,CONTEXT 自动解绑

Carrier 是不可变的、线程安全的。where() 方法返回一个新的 Carrier 对象,不会修改已有的映射。

4.4 ScopedValue 与 ThreadLocal 的核心差异

#mermaid-svg-M48mDa6x6Lb4Hh5s{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-M48mDa6x6Lb4Hh5s .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-M48mDa6x6Lb4Hh5s .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-M48mDa6x6Lb4Hh5s .error-icon{fill:#552222;}#mermaid-svg-M48mDa6x6Lb4Hh5s .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-M48mDa6x6Lb4Hh5s .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-M48mDa6x6Lb4Hh5s .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-M48mDa6x6Lb4Hh5s .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-M48mDa6x6Lb4Hh5s .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-M48mDa6x6Lb4Hh5s .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-M48mDa6x6Lb4Hh5s .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-M48mDa6x6Lb4Hh5s .marker{fill:#333333;stroke:#333333;}#mermaid-svg-M48mDa6x6Lb4Hh5s .marker.cross{stroke:#333333;}#mermaid-svg-M48mDa6x6Lb4Hh5s svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-M48mDa6x6Lb4Hh5s p{margin:0;}#mermaid-svg-M48mDa6x6Lb4Hh5s .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-M48mDa6x6Lb4Hh5s .cluster-label text{fill:#333;}#mermaid-svg-M48mDa6x6Lb4Hh5s .cluster-label span{color:#333;}#mermaid-svg-M48mDa6x6Lb4Hh5s .cluster-label span p{background-color:transparent;}#mermaid-svg-M48mDa6x6Lb4Hh5s .label text,#mermaid-svg-M48mDa6x6Lb4Hh5s span{fill:#333;color:#333;}#mermaid-svg-M48mDa6x6Lb4Hh5s .node rect,#mermaid-svg-M48mDa6x6Lb4Hh5s .node circle,#mermaid-svg-M48mDa6x6Lb4Hh5s .node ellipse,#mermaid-svg-M48mDa6x6Lb4Hh5s .node polygon,#mermaid-svg-M48mDa6x6Lb4Hh5s .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-M48mDa6x6Lb4Hh5s .rough-node .label text,#mermaid-svg-M48mDa6x6Lb4Hh5s .node .label text,#mermaid-svg-M48mDa6x6Lb4Hh5s .image-shape .label,#mermaid-svg-M48mDa6x6Lb4Hh5s .icon-shape .label{text-anchor:middle;}#mermaid-svg-M48mDa6x6Lb4Hh5s .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-M48mDa6x6Lb4Hh5s .rough-node .label,#mermaid-svg-M48mDa6x6Lb4Hh5s .node .label,#mermaid-svg-M48mDa6x6Lb4Hh5s .image-shape .label,#mermaid-svg-M48mDa6x6Lb4Hh5s .icon-shape .label{text-align:center;}#mermaid-svg-M48mDa6x6Lb4Hh5s .node.clickable{cursor:pointer;}#mermaid-svg-M48mDa6x6Lb4Hh5s .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-M48mDa6x6Lb4Hh5s .arrowheadPath{fill:#333333;}#mermaid-svg-M48mDa6x6Lb4Hh5s .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-M48mDa6x6Lb4Hh5s .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-M48mDa6x6Lb4Hh5s .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M48mDa6x6Lb4Hh5s .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-M48mDa6x6Lb4Hh5s .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M48mDa6x6Lb4Hh5s .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-M48mDa6x6Lb4Hh5s .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-M48mDa6x6Lb4Hh5s .cluster text{fill:#333;}#mermaid-svg-M48mDa6x6Lb4Hh5s .cluster span{color:#333;}#mermaid-svg-M48mDa6x6Lb4Hh5s div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-M48mDa6x6Lb4Hh5s .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-M48mDa6x6Lb4Hh5s rect.text{fill:none;stroke-width:0;}#mermaid-svg-M48mDa6x6Lb4Hh5s .icon-shape,#mermaid-svg-M48mDa6x6Lb4Hh5s .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M48mDa6x6Lb4Hh5s .icon-shape p,#mermaid-svg-M48mDa6x6Lb4Hh5s .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-M48mDa6x6Lb4Hh5s .icon-shape .label rect,#mermaid-svg-M48mDa6x6Lb4Hh5s .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M48mDa6x6Lb4Hh5s .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-M48mDa6x6Lb4Hh5s .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-M48mDa6x6Lb4Hh5s :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ScopedValue
不可变:绑定后不可修改
绑定到代码作用域
作用域内所有线程可见
作用域结束自动清理
ThreadLocal
可变:可以随时 set/get
绑定到线程生命周期
每个线程独立副本
需要手动 remove 清理

对比维度 ThreadLocal ScopedValue
可变性 可变(set()) 不可变(绑定后只读)
生命周期 绑定到线程 绑定到代码作用域
继承性 子线程不会自动继承 在 StructuredTaskScope 中自动继承
内存管理 需要手动 remove() 作用域结束自动释放
虚拟线程友好 ❌ 内存膨胀、上下文丢失 ✅ 专为虚拟线程设计
内存占用 每个线程独立副本 通过"进作用域才分配、出作用域即释放"将内存占用压至 ThreadLocal 的 1/10 以下

五、ScopedValue 实战用法

5.1 基础用法

java 复制代码
public class RequestHandler {
    
    // 1. 声明 ScopedValue,必须是 static final
    public static final ScopedValue<UserContext> USER_CONTEXT = ScopedValue.newInstance();
    
    public void handle(Request request) {
        UserContext context = buildContext(request);
        
        // 2. 绑定值并执行操作
        ScopedValue.where(USER_CONTEXT, context).run(() -> {
            // 在这个作用域内,任何方法都可以通过 USER_CONTEXT.get() 获取上下文
            processOrder();
        });
        // 3. 作用域结束后,绑定自动解除
    }
    
    private void processOrder() {
        // 4. 在作用域内获取值
        UserContext ctx = USER_CONTEXT.get();
        System.out.println("当前用户: " + ctx.userId());
    }
}

5.2 嵌套作用域

ScopedValue 支持嵌套绑定,内层作用域的值会覆盖外层,但外层值在内层作用域结束后恢复:

java 复制代码
ScopedValue.where(X, "hello").run(() -> {
    // X.get() == "hello"
    ScopedValue.where(X, "world").run(() -> {
        // X.get() == "world"
    });
    // X.get() == "hello"(恢复)
});

5.3 与 StructuredTaskScope 协同(自动继承)

ScopedValue 最重要的特性之一是:在 StructuredTaskScope 中 fork 的子任务会自动继承父作用域的 ScopedValue 绑定。

java 复制代码
public static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

ScopedValue.where(REQUEST_ID, "req-123").run(() -> {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        // 子任务自动继承 REQUEST_ID 绑定
        Supplier<String> userTask = scope.fork(() -> {
            // REQUEST_ID.get() == "req-123"
            return fetchUser();
        });
        
        Supplier<String> orderTask = scope.fork(() -> {
            // REQUEST_ID.get() == "req-123"
            return fetchOrders();
        });
        
        scope.join().throwIfFailed();
    } catch (Exception e) {
        // 处理异常
    }
});

这种继承是自动的 ,不需要像 InheritableThreadLocal 那样手动配置。而且继承的是绑定关系,不是值本身------值本身仍然是不可变的。

5.4 与并行流协同

在 Stream API 的并行流中,可以通过 runWhere() 为每个并行任务重新绑定上下文,确保数据隔离:

java 复制代码
orders.parallelStream().forEach(order -> {
    ScopedValue.where(DISCOUNT_CONTEXT, order.getDiscount()).run(() -> {
        // 每个并行任务拥有独立的上下文,不会串用
        double actualPrice = calculatePrice(order);
    });
});

六、迁移指南:从 ThreadLocal 到 ScopedValue

6.1 迁移原则

并非所有 ThreadLocal 都需要替换。迁移原则如下:
#mermaid-svg-4EjjbK9uRnhkIOGN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4EjjbK9uRnhkIOGN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4EjjbK9uRnhkIOGN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4EjjbK9uRnhkIOGN .error-icon{fill:#552222;}#mermaid-svg-4EjjbK9uRnhkIOGN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4EjjbK9uRnhkIOGN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4EjjbK9uRnhkIOGN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4EjjbK9uRnhkIOGN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4EjjbK9uRnhkIOGN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4EjjbK9uRnhkIOGN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4EjjbK9uRnhkIOGN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4EjjbK9uRnhkIOGN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4EjjbK9uRnhkIOGN .marker.cross{stroke:#333333;}#mermaid-svg-4EjjbK9uRnhkIOGN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4EjjbK9uRnhkIOGN p{margin:0;}#mermaid-svg-4EjjbK9uRnhkIOGN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4EjjbK9uRnhkIOGN .cluster-label text{fill:#333;}#mermaid-svg-4EjjbK9uRnhkIOGN .cluster-label span{color:#333;}#mermaid-svg-4EjjbK9uRnhkIOGN .cluster-label span p{background-color:transparent;}#mermaid-svg-4EjjbK9uRnhkIOGN .label text,#mermaid-svg-4EjjbK9uRnhkIOGN span{fill:#333;color:#333;}#mermaid-svg-4EjjbK9uRnhkIOGN .node rect,#mermaid-svg-4EjjbK9uRnhkIOGN .node circle,#mermaid-svg-4EjjbK9uRnhkIOGN .node ellipse,#mermaid-svg-4EjjbK9uRnhkIOGN .node polygon,#mermaid-svg-4EjjbK9uRnhkIOGN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4EjjbK9uRnhkIOGN .rough-node .label text,#mermaid-svg-4EjjbK9uRnhkIOGN .node .label text,#mermaid-svg-4EjjbK9uRnhkIOGN .image-shape .label,#mermaid-svg-4EjjbK9uRnhkIOGN .icon-shape .label{text-anchor:middle;}#mermaid-svg-4EjjbK9uRnhkIOGN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4EjjbK9uRnhkIOGN .rough-node .label,#mermaid-svg-4EjjbK9uRnhkIOGN .node .label,#mermaid-svg-4EjjbK9uRnhkIOGN .image-shape .label,#mermaid-svg-4EjjbK9uRnhkIOGN .icon-shape .label{text-align:center;}#mermaid-svg-4EjjbK9uRnhkIOGN .node.clickable{cursor:pointer;}#mermaid-svg-4EjjbK9uRnhkIOGN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4EjjbK9uRnhkIOGN .arrowheadPath{fill:#333333;}#mermaid-svg-4EjjbK9uRnhkIOGN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4EjjbK9uRnhkIOGN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4EjjbK9uRnhkIOGN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4EjjbK9uRnhkIOGN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4EjjbK9uRnhkIOGN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4EjjbK9uRnhkIOGN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4EjjbK9uRnhkIOGN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4EjjbK9uRnhkIOGN .cluster text{fill:#333;}#mermaid-svg-4EjjbK9uRnhkIOGN .cluster span{color:#333;}#mermaid-svg-4EjjbK9uRnhkIOGN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4EjjbK9uRnhkIOGN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4EjjbK9uRnhkIOGN rect.text{fill:none;stroke-width:0;}#mermaid-svg-4EjjbK9uRnhkIOGN .icon-shape,#mermaid-svg-4EjjbK9uRnhkIOGN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4EjjbK9uRnhkIOGN .icon-shape p,#mermaid-svg-4EjjbK9uRnhkIOGN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4EjjbK9uRnhkIOGN .icon-shape .label rect,#mermaid-svg-4EjjbK9uRnhkIOGN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4EjjbK9uRnhkIOGN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4EjjbK9uRnhkIOGN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4EjjbK9uRnhkIOGN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 跨调用链传递请求上下文
线程内缓存昂贵对象
线程隔离的计数器/状态
现有 ThreadLocal 使用场景
用途?
✅ 替换为 ScopedValue
❌ 不需要替换

虚拟线程下本就不应使用
✅ 替换为 ScopedValue 或

改用局部变量

6.2 代码迁移对比

迁移前(ThreadLocal) :

java 复制代码
public class UserContextHolder {
    private static final ThreadLocal<UserContext> CONTEXT = new ThreadLocal<>();
    
    public static void set(UserContext ctx) { CONTEXT.set(ctx); }
    public static UserContext get() { return CONTEXT.get(); }
    public static void remove() { CONTEXT.remove(); } // 必须手动清理
}

// 使用
UserContextHolder.set(context);
try {
    processOrder();
} finally {
    UserContextHolder.remove(); // 容易忘记
}

迁移后(ScopedValue) :

java 复制代码
public class UserContextHolder {
    public static final ScopedValue<UserContext> CONTEXT = ScopedValue.newInstance();
    
    // 不需要 set/get/remove 方法,API 本身已经足够简洁
}

// 使用
ScopedValue.where(UserContextHolder.CONTEXT, context).run(() -> {
    processOrder();
    // 作用域结束自动清理,不会忘记
});

6.3 日志框架的适配

这是一个容易踩坑的地方。Logback 1.4.x 及更早版本仍然使用 %X 和 MDC 依赖 ThreadLocal ,必须升级到 1.5.0+ 并启用 ScopedValue 感知适配器。

Logback 官方提供了 logback-scoped-mdc 模块,其中 ScopedMDC 由 java.lang.ScopedValue 支持,能够被 StructuredTaskScope 中 fork 的子任务自动继承,非常适合虚拟线程和结构化并发场景。

6.4 API 注意事项

在 JDK 25 正式版中,有一个 API 变更需要注意:ScopedValue.orElse(null) 不再允许传入 null 参数 。如果你的代码中使用了 orElse(null),需要调整为传入一个非 null 的默认值。

七、最佳实践

7.1 声明规范

  • 必须声明为 static final:ScopedValue 的设计意图就是作为共享的不可变上下文 key
  • 上下文对象本身也应设计为不可变:虽然 ScopedValue 保证绑定不可变,但对象内部的字段如果可变,仍然可能导致问题

7.2 作用域管理

java 复制代码
// ✅ 推荐:在虚拟线程创建点绑定作用域
// 例如在 Servlet 容器的 Connector 层,或 Spring 的 HandlerInterceptor 中
ScopedValue.where(REQUEST_CONTEXT, ctx).run(() -> {
    // 整个请求处理流程都在作用域内
    chain.doFilter(request, response);
});

// ❌ 不推荐:在过深的方法层级中绑定,作用域过窄

ScopedValue 的使用应该在虚拟线程的诞生点发起,即在 Servlet 容器(如 Tomcat 的 Connector)或框架入口层绑定,贯穿整个请求处理链路。

7.3 存在性检查

ScopedValue.get() 在未绑定时会抛出 NoSuchElementException,建议先检查:

java 复制代码
// ✅ 安全获取
public static UserContext getCurrentContext() {
    if (ScopedValue.bound(CONTEXT)) {
        return CONTEXT.get();
    }
    return UserContext.anonymous(); // 返回默认值
}

7.4 异步操作处理

ScopedValue 的作用域是同步代码块 。在 CompletableFuture 等异步场景中,如果任务在作用域结束后才执行,ScopedValue 绑定可能已经失效。此时需要手动重建作用域:

java 复制代码
ScopedValue.where(CONTEXT, ctx).run(() -> {
    CompletableFuture.supplyAsync(() -> {
        // 如果异步任务在作用域内执行,可以正常获取
        // 但需要确保执行器不是 ForkJoinPool.commonPool()
        return CONTEXT.get();
    }, virtualThreadExecutor);
});

八、总结

对比项 ThreadLocal ScopedValue
设计前提 线程池化 + 长期存活 + 少量线程 虚拟线程 + 短生命周期 + 海量线程
虚拟线程适配 ❌ 内存膨胀、上下文丢失 ✅ 专为虚拟线程设计
内存占用 每个虚拟线程独立副本 ThreadLocal 的 1/10 以下
清理方式 手动 remove() 作用域结束自动释放
子线程继承 需要 InheritableThreadLocal StructuredTaskScope 自动继承
引入版本 JDK 1.2 JDK 20 孵化 → JDK 21 预览 → JDK 25 正式

核心要点回顾:

  1. ThreadLocal 的设计前提与虚拟线程的根本特征不可调和------线程池化 vs 不池化、长期存活 vs 短生命周期、少量线程 vs 百万级线程
  2. ThreadLocal 在虚拟线程下的三大问题:内存膨胀(每个线程独立副本)、上下文丢失(线程跨载体调度)、内存残留(线程结束后 value 未释放)
  3. ScopedValue 是官方推荐的替代方案,核心优势:不可变、作用域受限、自动清理、自动继承
  4. 迁移原则 :跨调用链传递请求上下文的 ThreadLocal 替换为 ScopedValue;线程内缓存昂贵对象的 ThreadLocal 在虚拟线程下本就不应使用
  5. 日志框架需要适配:Logback 1.5.0+ 支持 ScopedValue 感知的 MDC 适配器
  6. JDK 25 已正式转正(JEP 506),可以放心在生产环境使用

如果你的项目正在使用虚拟线程,强烈建议将 ThreadLocal 的请求上下文传递逻辑迁移到 ScopedValue。这不仅能避免内存泄漏,还能获得更好的代码可读性和更安全的上下文管理。

相关推荐
普通人61 小时前
宝塔中实现按地区过滤和官网跳转伪静态
服务器·开发语言·nginx·1024程序员节
美味蛋炒饭.1 小时前
Java八股面试(4)
java·开发语言·面试
SL_staff1 小时前
OKR落地难?技术团队如何用五层联动链打通执行断层
java·开源·全栈
codeejun1 小时前
每日一Go·MySQL-7、MySQL 核心原理全景图谱
开发语言·mysql·golang
省钱兄--zs1 小时前
无人自助拼豆系统架构设计与功能实现落地部署全流程实战指南
java·数据仓库·spring boot·系统架构·需求分析
Raas1001 小时前
AI网关能做调用审计吗?MAI Gateway(魔芋企业级AI网关)实战能力深度解读
java·服务器·网络·数据库·gateway·prompt·企业级产品
海绵宝宝转agent1 小时前
2026-10-14算法+面经总结
java·笔记·算法·缓存
Java爱好狂.1 小时前
挑战一周速通Spring+SpringBoot+SpringCloud!
java·后端·spring·springboot·架构师·java面试·java程序员
小师兄吃牛肉2 小时前
SQL 编辑器异常排查:基于 KStudio 的一次实战记录
java·sql·编辑器