1. 概述
Scoped Values 允许开发者 在线程内部及跨线程间存储和共享不可变数据。这个新 API 作为孵化器预览功能在 Java 20 中引入,由 JEP 439 提出。
本文将首先比较 Scoped Values 与功能相似的旧 API 线程局部变量。接着探讨如何使用 Scoped Values 在线程间共享数据、重新绑定值以及在子线程中继承数据。然后通过经典 Web 框架案例展示实际应用。
最后说明如何在 Java 20 中启用该孵化器功能进行实验。
2. 设计动机
复杂 Java 应用通常包含多个需共享数据的模块和组件。当这些组件运行在多线程环境时,开发者需要一种在线程间共享不可变数据的方式。
但 不同线程可能需要不同数据,且不应能访问或修改其他线程的数据。
2.1. 线程局部变量
自 Java 1.2 起,我们可通过线程局部变量(ThreadLocal)在不依赖方法参数的情况下共享数据。线程局部变量本质上是 ThreadLocal 类型的特殊变量。
尽管它们看起来像普通变量,线程局部变量有多个实例,每个线程一个。具体使用哪个实例取决于调用 getter/setter 方法的线程。
线程局部变量通常声明为 public static 字段,方便多组件访问。
2.2. 现有方案的缺陷
尽管线程局部变量自 1998 年就存在,其 API 存在三大设计缺陷:
- 可变性风险:所有线程局部变量都可变,任何代码都能随时调用 setter 方法。导致数据在组件间双向流动,难以追踪共享状态更新顺序。
- 生命周期问题:使用 set 方法写入线程实例后,数据会持续存在直到线程结束或显式调用 remove。若忘记调用 remove,数据会不必要地长期占用内存。
- 继承开销:父线程的线程局部变量会被子线程继承。创建子线程时需为所有父线程局部变量分配额外存储空间。
2.3. 虚拟线程的挑战
Java 19 引入的虚拟线程使上述缺陷更加突出:
- 虚拟线程是由 JDK 而非操作系统管理的轻量级线程
- 大量虚拟线程可共享同一操作系统线程
- 若 百万级虚拟线程使用可变线程局部变量,内存占用将非常显著
因此 Java 20 推出 Scoped Values API,专为支持百万级虚拟线程设计,提供不可变且可继承的线程数据管理方案。
3. Scoped Values 核心机制
Scoped Values 实现 组件间安全高效共享不可变数据,无需方法参数传递。它与虚拟线程和结构化并发共同作为 Loom 项目的一部分。
3.1. 线程间数据共享
类似线程局部变量,Scoped Values 也为每个线程维护独立实例。通常声明为 public static 字段方便多组件访问:
public final static ScopedValue<User> LOGGED_IN_USER = ScopedValue.newInstance();
但关键区别在于 Scoped Values 写入后即不可变,且仅在执行线程的有限生命周期内有效:
ScopedValue.where(LOGGED_IN_USER, user.get()).run(
() -> service.getData()
);
where 方法需传入 Scoped Value 和要绑定的对象。调用 run 时:
- 创建当前线程独有的绑定实例
- 执行 lambda 表达式
在 run 方法执行期间,任何直接或间接调用的方法都能读取该值。但 run 结束后绑定立即销毁。
这种有限生命周期和不可变性特性极大简化了线程行为分析。不可变性带来更好性能,且数据只能单向传递:从调用方到被调用方。
3.2. 子线程继承机制
Scoped Values 会被使用 StructuredTaskScope 创建的所有子线程自动继承。子线程可直接使用父线程建立的绑定:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<Optional<Data>> internalData = scope.fork(
() -> internalService.getData(request)
);
Future<String> externalData = scope.fork(externalService::getData);
try {
scope.join();
scope.throwIfFailed();
Optional<Data> data = internalData.resultNow();
// 返回数据并设置HTTP状态码
} catch (InterruptedException | ExecutionException | IOException e) {
response.setStatus(500);
}
}
此时通过 fork 创建的子线程仍能访问父线程的 Scoped Values。但与线程局部变量不同,不会发生从父线程到子线程的数据复制。
3.3. 值的重新绑定
由于不可变性,Scoped Values 不支持 set 方法修改值。但我们可以 在限定代码段内重新绑定值:
例如通过 where 方法将值设为 null,临时隐藏该值:
ScopedValue.where(Server.LOGGED_IN_USER, null).run(service::extractData);
当代码段执行完毕,原始值立即恢复。注意 run 方法返回 void,若需处理返回值可改用 call 方法。
4. Web 框架实战案例
通过经典 Web 框架案例展示如何用 Scoped Values 共享登录用户数据。
4.1. 传统实现方式
Web 服务器认证请求后,需使登录用户数据对请求处理代码可用:
public void serve(HttpServletRequest request, HttpServletResponse response) throws InterruptedException, ExecutionException {
Optional<User> user = authenticateUser(request);
if (user.isPresent()) {
Future<?> future = executor.submit(() ->
controller.processRequest(request, response, user.get())
);
future.get();
} else {
response.setStatus(401);
}
}
控制器通过方法参数接收用户数据:
public void processRequest(HttpServletRequest request, HttpServletResponse response, User loggedInUser) {
Optional<Data> data = service.getData(request, loggedInUser);
// 返回数据并设置HTTP状态码
}
服务层接收但不使用用户数据,仅传递给仓库层:
public Optional<Data> getData(HttpServletRequest request, User loggedInUser) {
String id = request.getParameter("data_id");
return repository.getData(id, loggedInUser);
}
仓库层最终使用用户数据检查权限:
public Optional<Data> getData(String id, User loggedInUser) {
return loggedInUser.isAdmin()
? Optional.of(new Data(id, "Title 1", "Description 1"))
: Optional.empty();
}
在复杂应用中,请求处理可能涉及大量方法。即使只有少数方法需要用户数据,也必须层层传递,导致:
- ✅ 代码冗余
- ❌ 方法参数过多(超过推荐值3个)
- ⚠️ 维护困难
4.2. 使用 Scoped Values 优化
将登录用户数据存储在 Scoped Value 中,任何方法均可直接访问:
public void serve(HttpServletRequest request, HttpServletResponse response) {
Optional<User> user = authenticateUser(request);
if (user.isPresent()) {
ScopedValue.where(LOGGED_IN_USER, user.get())
.run(() -> controller.processRequest(request, response));
} else {
response.setStatus(401);
}
}
现在可移除所有方法中的 loggedInUser 参数:
public void processRequest(HttpServletRequest request, HttpServletResponse response) {
Optional<Data> data = internalService.getData(request);
// 返回数据并设置HTTP状态码
}
服务层无需传递用户数据:
public Optional<Data> getData(HttpServletRequest request) {
String id = request.getParameter("data_id");
return repository.getData(id);
}
仓库层通过 Scoped Value 的 get 方法获取用户数据:
public Optional<Data> getData(String id) {
User loggedInUser = Server.LOGGED_IN_USER.get();
return loggedInUser.isAdmin()
? Optional.of(new Data(id, "Title 1", "Description 1"))
: Optional.empty();
}
使用 Scoped Values 后代码更简洁易维护。
4.3. 启用孵化器预览功能
在 Java 20 中运行上述示例需 启用预览功能并添加并发孵化器模块:
$ javac --enable-preview -source 20 --add-modules jdk.incubator.concurrent *.java
$ java --enable-preview --add-modules jdk.incubator.concurrent Server.class
Maven 项目需在 编译器插件和 测试插件中添加相同参数:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>20</source>
<target>20</target>
<compilerArgs>
<arg>--enable-preview</arg>
<arg>--add-modules=jdk.incubator.concurrent</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>--enable-preview --add-modules=jdk.incubator.concurrent</argLine>
</configuration>
</plugin>
5. 总结
本文深入探讨了 Java 20 的孵化器预览功能 Scoped Values。通过对比线程局部变量,阐明了创建新 API 用于在线程内外共享不可变数据的动机。
我们学习了如何使用 Scoped Values:
- 在线程间共享数据
- 重新绑定值
- 在子线程中继承数据
并通过 Web 框架案例展示了共享登录用户数据的最佳实践。最后说明了在 Java 20 中启用孵化器预览功能进行实验的方法。
完整源代码见 GitHub。