ScopedValue是Java并发包中引入的新特性,主要面向虚拟线程场景设计,用于在线程执行范围内传递上下文信息,它的不可变性设计和ThreadLocal有本质区别,在虚拟线程大量创建的场景下能带来明显的性能提升。

ScopedValue的不可变性含义
ScopedValue的不可变性指的是一旦ScopedValue被绑定了值,在整个绑定的作用域内这个值无法被修改,也不能被重新绑定,直到作用域结束才会自动解除绑定。这和ThreadLocal可以随时调用set方法修改存储值的特性完全不同。
ScopedValue的绑定操作是通过ScopedValue.where方法完成的,绑定之后只能在当前作用域内读取,不能修改。下面是一个简单的ScopedValue使用示例:
import java.lang.ScopedValue;
public class ScopedValueDemo {
// 定义一个ScopedValue实例
private static final ScopedValue<String> USER_NAME = ScopedValue.newInstance();
public static void main(String[] args) {
// 绑定值到ScopedValue,作用域为run方法内的代码块
ScopedValue.where(USER_NAME, "test_user")
.run(() -> {
// 在作用域内读取绑定的值
String name = USER_NAME.get();
System.out.println("当前用户名称:" + name);
// 尝试修改会编译报错,ScopedValue没有set方法
// USER_NAME.set("new_user"); 这行代码无法编译通过
});
// 作用域外无法读取到绑定的值,会抛出异常
try {
USER_NAME.get();
} catch (NoSuchElementException e) {
System.out.println("作用域外无法获取ScopedValue的值");
}
}
}
ThreadLocal在虚拟线程中的问题
ThreadLocal的设计是基于线程实例存储数据的,每个线程都有自己独立的ThreadLocalMap,存储对应ThreadLocal实例的值。在传统的平台线程场景下,线程数量有限,ThreadLocal的开销可以忽略不计。但在虚拟线程场景下,虚拟线程是轻量级的,可能会被大量创建和销毁,ThreadLocal会带来两个问题:
- 内存开销大:每个虚拟线程都会维护自己的ThreadLocalMap,大量虚拟线程会占用大量内存,而且ThreadLocal的值如果没有及时清理,还可能导致内存泄漏。
- 绑定开销高:ThreadLocal的set和get操作需要操作线程本地的Map,虚拟线程的调度频繁,每次操作的开销会被放大。
ScopedValue相比ThreadLocal的性能优势
ScopedValue在虚拟线程中的性能优势主要来源于它的不可变性设计和作用域绑定机制,具体体现在以下几个方面:
1. 更低的内存占用
ScopedValue不需要每个虚拟线程维护独立的存储结构,它的绑定信息是和作用域关联的,作用域结束后自动清理,不会像ThreadLocal那样因为虚拟线程的创建销毁产生大量冗余的内存占用。下面是两者的内存占用对比:
| 对比项 | ThreadLocal | ScopedValue |
|---|---|---|
| 存储结构归属 | 每个线程独立维护ThreadLocalMap | 绑定信息关联作用域,不归属线程 |
| 大量虚拟线程场景内存占用 | 高,每个虚拟线程都有独立的Map | 低,作用域结束自动回收 |
| 内存泄漏风险 | 高,未及时remove可能导致泄漏 | 无,作用域结束自动解除绑定 |
2. 更快的读写速度
ScopedValue的读取操作不需要像ThreadLocal那样查找线程本地的Map,它的绑定信息在作用域内是可直接访问的,读写开销更低。我们可以通过一个简单的性能测试对比两者的耗时:
import java.lang.ScopedValue;
import java.util.concurrent.ThreadLocalRandom;
public class PerformanceTest {
private static final ThreadLocal<Integer> THREAD_LOCAL = ThreadLocal.withInitial(() -> 0);
private static final ScopedValue<Integer> SCOPED_VALUE = ScopedValue.newInstance();
public static void main(String[] args) {
int loopCount = 1000000;
// 测试ThreadLocal读写耗时
long threadLocalStart = System.currentTimeMillis();
for (int i = 0; i < loopCount; i++) {
THREAD_LOCAL.set(ThreadLocalRandom.current().nextInt());
int val = THREAD_LOCAL.get();
THREAD_LOCAL.remove();
}
long threadLocalEnd = System.currentTimeMillis();
System.out.println("ThreadLocal 100万次读写耗时:" + (threadLocalEnd - threadLocalStart) + "ms");
// 测试ScopedValue读写耗时
long scopedValueStart = System.currentTimeMillis();
for (int i = 0; i < loopCount; i++) {
int finalI = i;
ScopedValue.where(SCOPED_VALUE, ThreadLocalRandom.current().nextInt())
.run(() -> {
int val = SCOPED_VALUE.get();
});
}
long scopedValueEnd = System.currentTimeMillis();
System.out.println("ScopedValue 100万次读写耗时:" + (scopedValueEnd - scopedValueStart) + "ms");
}
}
在虚拟线程大量创建的场景下,ScopedValue的读写耗时通常会比ThreadLocal低30%以上,因为省去了线程本地Map的查找和修改开销。
3. 更适配虚拟线程的调度模型
虚拟线程是挂载在平台线程上执行的,可能会发生频繁的挂载和卸载,ThreadLocal的存储是和虚拟线程绑定的,调度过程中需要维护这些绑定关系。而ScopedValue的绑定是和作用域绑定的,和虚拟线程的调度无关,不会因为虚拟线程的切换产生额外的开销。
使用场景建议
如果你的应用使用的是虚拟线程,并且需要在执行范围内传递上下文信息,且上下文的值在作用域内不需要修改,那么优先选择ScopedValue。如果是在传统的平台线程场景,或者需要随时修改上下文的值,那么ThreadLocal仍然是合适的选择。
注意:ScopedValue是Java 21中正式引入的特性,使用前需要确保JDK版本不低于21。
ScopedValueThreadLocal虚拟线程不可变性修改时间:2026-06-13 20:21:33