一、前言
虚拟线程(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 的缓存模式之所以高效,建立在两个关键假设之上:
- 线程是长期存活的(应用启动时创建,运行期间一直存在)
- 线程被大量复用(一个线程处理成千上万个请求)
在这种情况下,一个昂贵的对象(如 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 正式 |
核心要点回顾:
- ThreadLocal 的设计前提与虚拟线程的根本特征不可调和------线程池化 vs 不池化、长期存活 vs 短生命周期、少量线程 vs 百万级线程
- ThreadLocal 在虚拟线程下的三大问题:内存膨胀(每个线程独立副本)、上下文丢失(线程跨载体调度)、内存残留(线程结束后 value 未释放)
- ScopedValue 是官方推荐的替代方案,核心优势:不可变、作用域受限、自动清理、自动继承
- 迁移原则 :跨调用链传递请求上下文的
ThreadLocal替换为ScopedValue;线程内缓存昂贵对象的ThreadLocal在虚拟线程下本就不应使用 - 日志框架需要适配:Logback 1.5.0+ 支持 ScopedValue 感知的 MDC 适配器
- JDK 25 已正式转正(JEP 506),可以放心在生产环境使用
如果你的项目正在使用虚拟线程,强烈建议将 ThreadLocal 的请求上下文传递逻辑迁移到 ScopedValue。这不仅能避免内存泄漏,还能获得更好的代码可读性和更安全的上下文管理。