写多线程程序的时候,经常需要在同一个线程里传递上下文数据,比如用户信息、请求ID、事务上下文这些,传统方式用ThreadLocal,但是ThreadLocal有好多问题,可变性、生命周期不受限制、性能开销大,特别是用虚拟线程的时候,问题更明显。JDK 23的JEP 481整了个新活,引入了作用域值(Scoped Values),作为ThreadLocal的现代化替代方案。
鹏磊我之前做Web框架的时候,经常要用ThreadLocal传递请求上下文,但是ThreadLocal的值可以改,生命周期也不受控制,有时候线程池复用了,ThreadLocal的值没清理,就出bug了。而且用虚拟线程的时候,ThreadLocal的性能开销也大,因为虚拟线程数量多,每个线程都有自己的ThreadLocal存储。
现在有了作用域值,数据是不可变的,生命周期被限制在代码块里,自动管理,性能也好,特别是配合虚拟线程和结构化并发用,效果更好。今天咱就好好聊聊这个作用域值,看看它到底整了哪些活,怎么用才能发挥最大效果。
JEP 481 的核心概念
作用域值(Scoped Values)是一种机制,允许方法在同一线程内与其被调用的方法以及子线程共享不可变数据。与ThreadLocal相比,作用域值更易于理解,性能更好,特别是在虚拟线程和结构化并发的场景下。
作用域值的特点
作用域值有几个核心特点:
- 不可变性:作用域值一旦绑定就不能修改,确保数据安全
- 生命周期受限:作用域值的生命周期被限制在代码块里,自动管理
- 性能优化:与虚拟线程配合使用,空间和时间成本更低
- 结构化访问:数据的访问范围从代码结构上就能看出来
// 传统方式,用ThreadLocal
private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();
void handleRequest(Request request) {
USER_ID.set(request.getUserId()); // 可以设置
try {
processRequest(request);
} finally {
USER_ID.remove(); // 得手动清理,容易忘记
}
}
void processRequest(Request request) {
String userId = USER_ID.get(); // 获取值
USER_ID.set("new value"); // 可以修改,不安全
// ...
}
// 作用域值方式
private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
void handleRequest(Request request) {
ScopedValue.runWhere(USER_ID, request.getUserId(), () -> {
processRequest(request); // 在这个作用域里,USER_ID可用
}); // 作用域结束,自动清理
}
void processRequest(Request request) {
String userId = USER_ID.get(); // 获取值
// USER_ID不能修改,不可变
// ...
}
作用域值让代码更安全,资源管理也更简单。
ThreadLocal的问题
ThreadLocal有几个问题,作用域值都解决了:
- 可变性:ThreadLocal的值可以修改,可能导致数据不一致
- 生命周期不受限制:ThreadLocal的值可能在线程池复用的时候泄漏
- 性能开销:虚拟线程数量多的时候,ThreadLocal的存储开销大
- 难以理解:数据的生命周期不清晰,容易出bug
// ThreadLocal的问题示例
private static final ThreadLocal<StringBuilder> BUFFER = new ThreadLocal<>();
void process() {
StringBuilder buffer = BUFFER.get();
if (buffer == null) {
buffer = new StringBuilder();
BUFFER.set(buffer);
}
buffer.append("data"); // 可以修改,不安全
// 如果线程被复用,buffer可能还保留着之前的数据
// 得手动清理,容易忘记
}
作用域值解决了这些问题,数据不可变,生命周期自动管理。
ScopedValue API
作用域值的核心API是ScopedValue类,提供了创建、绑定和获取值的方法。
创建作用域值
使用ScopedValue.newInstance()创建作用域值:
// 创建作用域值
private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
private static final ScopedValue<RequestContext> CONTEXT = ScopedValue.newInstance();
private static final ScopedValue<Integer> REQUEST_ID = ScopedValue.newInstance();
作用域值通常是静态final字段,在整个应用生命周期内存在。
绑定值到作用域
使用ScopedValue.runWhere()或ScopedValue.callWhere()绑定值到作用域:
// 使用runWhere绑定值并执行Runnable
ScopedValue.runWhere(USER_ID, "user123", () -> {
// 在这个作用域里,USER_ID.get()返回"user123"
processUser();
}); // 作用域结束,值自动清理
// 使用callWhere绑定值并执行Callable
String result = ScopedValue.callWhere(USER_ID, "user123", () -> {
// 在这个作用域里,USER_ID.get()返回"user123"
return processUserAndReturn();
});
值只在绑定的作用域内可用,作用域结束就自动清理。
获取值
在作用域内使用get()方法获取值:
void processUser() {
String userId = USER_ID.get(); // 获取当前作用域的值
println("Processing user: " + userId);
// 如果值未绑定,get()会抛出异常
// 可以用isBound()检查,或者用orElse()提供默认值
}
void processUserSafely() {
if (USER_ID.isBound()) {
String userId = USER_ID.get();
println("Processing user: " + userId);
} else {
println("No user ID available");
}
// 或者用orElse提供默认值
String userId = USER_ID.orElse("anonymous");
println("Processing user: " + userId);
}
嵌套作用域
作用域可以嵌套,内层作用域可以覆盖外层作用域的值:
// 嵌套作用域
ScopedValue.runWhere(USER_ID, "user123", () -> {
println(USER_ID.get()); // 输出: user123
ScopedValue.runWhere(USER_ID, "user456", () -> {
println(USER_ID.get()); // 输出: user456,覆盖了外层值
});
println(USER_ID.get()); // 输出: user123,恢复外层值
});
内层作用域的值会覆盖外层,但是外层值不会丢失,内层作用域结束后会恢复。
与虚拟线程结合
作用域值特别适合和虚拟线程配合使用,性能好,资源占用少。
虚拟线程的优势
虚拟线程是轻量级线程,可以创建大量虚拟线程,但是ThreadLocal在虚拟线程场景下有问题:
// ThreadLocal在虚拟线程场景下的问题
private static final ThreadLocal<String> DATA = new ThreadLocal<>();
void processRequests(List<Request> requests) {
for (Request request : requests) {
Thread.ofVirtual().start(() -> {
DATA.set(request.getId()); // 每个虚拟线程都有自己的ThreadLocal存储
processRequest(request);
DATA.remove(); // 得手动清理
});
}
// 虚拟线程数量多,ThreadLocal存储开销大
}
作用域值在虚拟线程场景下性能更好:
// 作用域值在虚拟线程场景下
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
void processRequests(List<Request> requests) {
for (Request request : requests) {
Thread.ofVirtual().start(() -> {
ScopedValue.runWhere(REQUEST_ID, request.getId(), () -> {
processRequest(request); // 在作用域内,REQUEST_ID可用
}); // 作用域结束,自动清理
});
}
// 作用域值的存储开销小,性能好
}
与结构化并发结合
作用域值可以和结构化并发(JEP 480)配合使用,在并发任务间共享数据:
// 作用域值和结构化并发结合
private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
void processUserData(String userId) {
ScopedValue.runWhere(USER_ID, userId, () -> {
try (var scope = new StructuredTaskScope<String>()) {
// 在作用域内,所有子任务都可以访问USER_ID
var task1 = scope.fork(() -> {
String id = USER_ID.get(); // 子任务可以访问作用域值
return fetchUserProfile(id);
});
var task2 = scope.fork(() -> {
String id = USER_ID.get(); // 子任务可以访问作用域值
return fetchUserOrders(id);
});
scope.join();
UserProfile profile = task1.get();
List<Order> orders = task2.get();
return new UserData(profile, orders);
}
});
}
作用域值可以在结构化并发的子任务间共享,数据传递更方便。
实际应用场景
场景1:Web框架的请求上下文
Web框架需要在不同方法间传递请求上下文,用作用域值很方便:
// Web框架的请求上下文
public class WebFramework {
private static final ScopedValue<RequestContext> CONTEXT = ScopedValue.newInstance();
void serve(Request request, Response response) {
RequestContext context = createContext(request);
ScopedValue.runWhere(CONTEXT, context, () -> {
Application.handle(request, response); // 在作用域内,CONTEXT可用
}); // 作用域结束,自动清理
}
// 在应用代码里,可以直接获取上下文
public static PersistedObject readKey(String key) {
RequestContext context = CONTEXT.get(); // 获取当前请求的上下文
Database db = getDBConnection(context);
return db.readKey(key);
}
}
这样写代码清晰,资源管理也安全。
场景2:日志追踪
在日志追踪场景,需要在调用链中传递追踪ID:
// 日志追踪
public class Tracing {
private static final ScopedValue<String> TRACE_ID = ScopedValue.newInstance();
void handleRequest(Request request) {
String traceId = generateTraceId();
ScopedValue.runWhere(TRACE_ID, traceId, () -> {
log("Request started: " + traceId);
processRequest(request);
log("Request completed: " + traceId);
});
}
void processRequest(Request request) {
String traceId = TRACE_ID.get(); // 获取追踪ID
log("Processing: " + traceId);
callService1(); // 调用其他服务
callService2();
}
void callService1() {
String traceId = TRACE_ID.get(); // 在调用链中传递追踪ID
log("Calling service1: " + traceId);
}
void log(String message) {
String traceId = TRACE_ID.orElse("unknown");
System.out.println("[" + traceId + "] " + message);
}
}
追踪ID在调用链中自动传递,不用显式传参。
场景3:事务上下文
在事务管理场景,需要在不同方法间传递事务上下文:
// 事务上下文
public class TransactionManager {
private static final ScopedValue<Transaction> TX = ScopedValue.newInstance();
<T> T executeInTransaction(Supplier<T> operation) {
Transaction tx = beginTransaction();
return ScopedValue.callWhere(TX, tx, () -> {
try {
T result = operation.get(); // 执行操作
tx.commit(); // 提交事务
return result;
} catch (Exception e) {
tx.rollback(); // 回滚事务
throw e;
}
});
}
// 在业务代码里,可以直接获取事务
void saveData(Data data) {
Transaction tx = TX.get(); // 获取当前事务
tx.execute("INSERT INTO ...", data);
}
}
事务上下文在作用域内自动传递,不用显式传参。
场景4:用户认证信息
在用户认证场景,需要在不同方法间传递用户信息:
// 用户认证信息
public class AuthManager {
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
void handleAuthenticatedRequest(Request request, User user) {
ScopedValue.runWhere(CURRENT_USER, user, () -> {
processRequest(request); // 在作用域内,CURRENT_USER可用
});
}
// 在业务代码里,可以直接获取当前用户
void processRequest(Request request) {
User user = CURRENT_USER.get(); // 获取当前用户
if (user.hasPermission("read")) {
readData();
}
}
void readData() {
User user = CURRENT_USER.get(); // 在调用链中传递用户信息
log("User " + user.getId() + " reading data");
}
}
用户信息在调用链中自动传递,代码更简洁。
与传统ThreadLocal的对比
代码对比
看个完整的例子,对比一下代码:
// 传统方式,用ThreadLocal
public class TraditionalApproach {
private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();
void handleRequest(Request request) {
USER_ID.set(request.getUserId());
try {
processRequest(request);
} finally {
USER_ID.remove(); // 得手动清理
}
}
void processRequest(Request request) {
String userId = USER_ID.get();
USER_ID.set("modified"); // 可以修改,不安全
// ...
}
}
// 作用域值方式
public class ScopedValueApproach {
private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
void handleRequest(Request request) {
ScopedValue.runWhere(USER_ID, request.getUserId(), () -> {
processRequest(request); // 在作用域内,USER_ID可用
}); // 作用域结束,自动清理
}
void processRequest(Request request) {
String userId = USER_ID.get();
// USER_ID不能修改,不可变,安全
// ...
}
}
作用域值方式代码更简洁,资源管理也更安全。
性能对比
在虚拟线程场景下,作用域值的性能更好:
// ThreadLocal方式,性能开销大
private static final ThreadLocal<String> DATA = new ThreadLocal<>();
void processManyRequests(List<Request> requests) {
requests.parallelStream().forEach(request -> {
Thread.ofVirtual().start(() -> {
DATA.set(request.getId()); // 每个虚拟线程都有自己的ThreadLocal存储
processRequest(request);
DATA.remove();
});
});
// 虚拟线程数量多,ThreadLocal存储开销大
}
// 作用域值方式,性能开销小
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
void processManyRequests(List<Request> requests) {
requests.parallelStream().forEach(request -> {
Thread.ofVirtual().start(() -> {
ScopedValue.runWhere(REQUEST_ID, request.getId(), () -> {
processRequest(request); // 作用域值的存储开销小
});
});
});
// 作用域值的存储开销小,性能好
}
作用域值在虚拟线程场景下性能更好,因为存储开销小。
安全性对比
作用域值更安全,因为数据不可变:
// ThreadLocal方式,数据可变,不安全
private static final ThreadLocal<StringBuilder> BUFFER = new ThreadLocal<>();
void process() {
StringBuilder buffer = BUFFER.get();
if (buffer == null) {
buffer = new StringBuilder();
BUFFER.set(buffer);
}
buffer.append("data"); // 可以修改,可能导致数据不一致
}
// 作用域值方式,数据不可变,安全
private static final ScopedValue<String> DATA = ScopedValue.newInstance();
void process() {
ScopedValue.runWhere(DATA, "initial", () -> {
String data = DATA.get(); // 获取值
// DATA不能修改,不可变,安全
});
}
作用域值的数据不可变,更安全。
启用预览特性
JEP 481是预览特性,得先启用才能用:
# 编译时启用预览特性
javac --enable-preview --release 23 MyClass.java
# 运行时启用预览特性
java --enable-preview MyClass
Maven配置
如果用Maven,可以在pom.xml里配置:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>23</release>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
模块依赖
如果项目是模块化的,需要在module-info.java里声明依赖:
module com.example.myapp {
requires java.base;
// ScopedValue在java.base模块里(JDK 23)
}
最佳实践
1. 使用静态final字段
作用域值应该声明为静态final字段:
// 好的做法:静态final字段
private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
// 不好的做法:非静态字段
private ScopedValue<String> userId = ScopedValue.newInstance(); // 每个实例都有自己的作用域值,浪费
2. 使用try-with-resources模式
虽然作用域值不是AutoCloseable,但是可以用类似的方式组织代码:
// 好的做法:清晰的作用域
void handleRequest(Request request) {
ScopedValue.runWhere(USER_ID, request.getUserId(), () -> {
processRequest(request);
}); // 作用域结束,自动清理
}
// 不好的做法:作用域不清晰
void handleRequest(Request request) {
ScopedValue.runWhere(USER_ID, request.getUserId(), () -> {
processRequest(request);
// 很多其他代码,作用域不清晰
});
}
3. 检查值是否绑定
如果值可能未绑定,应该检查:
// 好的做法:检查值是否绑定
void process() {
if (USER_ID.isBound()) {
String userId = USER_ID.get();
processUser(userId);
} else {
processAnonymous();
}
}
// 或者用orElse提供默认值
void process() {
String userId = USER_ID.orElse("anonymous");
processUser(userId);
}
4. 避免在作用域外使用
不要在作用域外尝试获取值:
// 错误:在作用域外使用
void handleRequest(Request request) {
ScopedValue.runWhere(USER_ID, request.getUserId(), () -> {
processRequest(request);
});
String userId = USER_ID.get(); // 错误!作用域已经结束了
}
// 正确:在作用域内使用
void handleRequest(Request request) {
ScopedValue.runWhere(USER_ID, request.getUserId(), () -> {
processRequest(request);
String userId = USER_ID.get(); // 正确,在作用域内
});
}
总结
JEP 481引入的作用域值,确实让线程本地存储变得更现代化了。特别是不可变性和生命周期管理,让代码更安全,资源管理也更简单。
鹏磊我觉得这个特性还是挺有用的,特别是那些需要在调用链中传递上下文数据的场景,用作用域值代码清晰,性能也好。配合虚拟线程和结构化并发用,效果更好。
总的来说,作用域值是ThreadLocal的现代化替代方案,让线程本地存储变得更安全、更高效。虽然现在还是预览特性,但是未来应该会稳定下来,成为Java并发编程的常用工具。