Java 25 新特性学习 - Scoped Values
Java 25 是一个 LTS 版本,它的众多新特性中就数 JEP 506: Scoped Values 能改变我们从前使用
ThreadLocal 时的编程范式。因其重要性,所以单列一篇来专门学习它。Scoped Values, 可用来替代 ThreadLocal 的使用,特别是在虚拟线程当中。
Scoped Values 用于在当前线程或子线程中共享不可变的数据,说到不可变的数据就会想到 JEP 502: Stable Values (Preview).
ThreadLocal, 甚至 InheritableThreadLocal 用于子线程中共享数据还是有些挑战性,特别碰到线程池的情况, 而且 ThreadLocal
不在乎数据可变还是不可变的,执行当中谁改了数据,不知道。Scoped Values 的 JEP 说其目的不是用来完全替代 ThreadLocal。
下面来看看 Scoped Values 如何在子线程中共享数据,它还能与虚拟线程的结构化并发配合使用。
ThreadLocal/InteritableThreadLocal 仍然是很多框架用来在线程中(间)共享数据的办法,在 Spring 框架中有大量的使用, 如
XxxContextHolder 之类的。目前的 ThreadLocal 存在一些问题
- ThreadLocal 中的数据是可变的,多数应用场景只要不可变的数据
- 难以管理共享数据的生命周期,特别是在线程池中,线程被重用时,ThreadLocal 中的数据可能会被意外共享或泄露。由使用方主动清除数据,更是会造成潜在的 Bug
- 成本高昂,创建子线程,或线程切换时要对共享数据进行复制,
VirtualThread也是继承自Thread, 所以虚拟线程也能用ThreadLocal。 一旦虚拟线程的数量达到成千上万,十百万的级别时,ThreadLocal数据不停复制的代价就很高了
Scroped Values 就是设计来解决以上问题的,减少像 ThreadLocal 的复杂性; 数据为不可变的话,就能更高效的共享,特别是在有巨量的虚拟线程时;
数据在超过共享期后自动清除。
除去成本来说,个人觉得 ThreadLocal 用起来还是很方便的,可以让两个不怎么相关的代码共享数据。比如用 ThreadLocal 时,共享代码的方式为
1public class Test {
2 public static void main(String[] args) {
3 A.foo();
4 }
5}
6
7class A {
8 public static void foo() {
9 Holder.set("value a");
10 B.bar();
11 }
12}
13
14class B {
15 public static void bar() {
16 System.out.println(Holder.get()); // value a
17 }
18}
19
20class Holder {
21 private static final ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "default");
22
23 public static String get() {
24 return threadLocal.get();
25 }
26
27 public static void set(String value) {
28 threadLocal.set(value);
29 }
30}
就是使用线程池的时候,经常要把线程绑定的变量手动复制并绑定到任务子线程,任务执行完后还得从子线程上清除掉。这就是 Spring 的 TaskDecorator
经常做的,比如把 slf4j 的 MDC(Mapped Diagnostic Context) 数据复制到子线程中的做法
1ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
2executor.setTaskDecorator(runnable -> {
3 Map<String, String> mdcContextMap = MDC.getCopyOfContextMap();
4 if (mdcContextMap == null) {
5 return runnable;
6 }
7 return () -> {
8 try {
9 MDC.setContextMap(mdcContextMap);
10 runnable.run();
11 } finally {
12 MDC.clear();
13 }
14 };
15});
倒是想看看 Scoped Values 如何在线程中共享数据。用 Claude 告诉它用 Scoped Value 来重构上面的代码的话,会被改成
1class A {
2 public static void foo() {
3 ScopedValue.where(Holder.VALUE, "value a").run(B::bar);
4 }
5}
6
7class B {
8 public static void bar() {
9 System.out.println(Holder.VALUE.orElse("default")); // value a
10 }
11}
12
13class Holder {
14 static final ScopedValue<String> VALUE = ScopedValue.newInstance();
15}
Test 类不变,用 Scoped Values 关键就在 ScopedValue.where() 和 ScopedValue.run() 上。这两方法的原型为
1 public static <T> Carrier where(ScopedValue<T> key, T value) {
2 return Carrier.of(key, value);
3 }
4
5 Carrier.run(Runnable op);
6 Carrier.call(CallableOp<? extends R, X> op) throws X;
这种简单问题用 Scoped Value 好像解决的不错,可是很多现实问题,比如在方法 a 与 b 经常许多的方法调用,代码跨越了很多类,就不是一个
1ScopedValue.where(Holder.VALUE, "value a").run(() -> {
2 // method a
3 // method b
4});
能简单解决的。再如果要共享更多的数据,可能会写出 ScropedValue.where() 嵌套出来
1ScopedValue.where(Holder.VALUE1, "value 1").run(()-> {
2 ScopedValue.where(Holder.VALUE2, "value 2").run(() -> {
3 // method a
4 // method b
5 ...
6 });
7});
想要跨线程如何共享数据呢?尝试下面的代码
1public class Test {
2 static final ScopedValue<Object> VALUE = ScopedValue.newInstance();
3
4 public static void main(String[] args) {
5 var obj = new Object();
6 System.out.println(obj);
7 ScopedValue.where(VALUE, obj).run(()->{
8 new Thread(()-> {
9 System.out.println(VALUE.get());
10 }).start();
11 });
12 }
13}
执行出错
1Exception in thread "Thread-0" java.util.NoSuchElementException: ScopedValue not bound
2 at java.base/java.lang.ScopedValue.slowGet(ScopedValue.java:571)
3 at java.base/java.lang.ScopedValue.get(ScopedValue.java:564)
4 at Test.lambda$main$1(Test.java:9)
5 at java.base/java.lang.Thread.run(Thread.java:1474)
如果用 InteritableThreadLocal 是可解决这个问题的。
继续阅读 JEP 506.
对于一个应用框架,如 SpringBoot Web 框架,每个请求都必经一些关口,如 org.springframework.web.servlet.service(request, response)
方法,或各级 RequestFilter 的 doFilter() 都可以开始一个 Scope, 然后自己写的 Controller 方法自然而然的涵盖进了这个 Scope,
也就能访问到其中的共享数据,等 servlet.service() 或 doFilter() 结束后共享数据自动被清除,这样就不想考虑往一个 HTTP 线程放进去的
ThreadLocal 数据会不会污染到另一个 HTTP 请求。

对于我们最容易的使用方式应该是在自定义的 RequestFilter 中, 下面就试着用 Scoped Value 来重构一个 SpringBoot Web 程序的就用场景,
请求进入后,首后获得或生成 requestId 和 userId, 然后在该 HTTP 线程上随时可用, 三个代码来演示
RequestContext
1package yanbin.blog;
2
3public record RequestContext(String requestId, String userId) {
4 static ScopedValue<RequestContext> holder = ScopedValue.newInstance();
5
6 public static String getRequestId() {
7 return holder.get().requestId;
8 }
9
10 public static String getUserId() {
11 return holder.get().userId;
12 }
13}
让 CustomRequestFilter 和 RequestContext 同处一个包中,这样在 CustomRequestFilter 中可以访问到 holder, 其他地方只能调用
RequestContext.getRequestId() 和 RequestContext.getUserId() 来访问共享数据了。
CustomRequestFilter
1package yanbin.blog;
2
3import jakarta.servlet.FilterChain;
4import jakarta.servlet.http.HttpServletRequest;
5import jakarta.servlet.http.HttpServletResponse;
6import org.jspecify.annotations.NonNull;
7import org.springframework.core.Ordered;
8import org.springframework.core.annotation.Order;
9import org.springframework.stereotype.Component;
10import org.springframework.web.filter.OncePerRequestFilter;
11
12import java.util.UUID;
13
14@Component
15@Order(Ordered.HIGHEST_PRECEDENCE)
16public class CustomRequestFilter extends OncePerRequestFilter {
17
18 @Override
19 protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
20 @NonNull FilterChain filterChain) {
21
22 ScopedValue.where(RequestContext.holder,
23 new RequestContext(UUID.randomUUID().toString(), "user-1234"))
24 .run(() -> {
25 try {
26 filterChain.doFilter(request, response);
27 } catch (Exception e) {
28 throw new RuntimeException(e);
29 }
30 });
31 }
32}
TestController
1package yanbin.blog;
2
3import org.springframework.web.bind.annotation.GetMapping;
4import org.springframework.web.bind.annotation.RestController;
5
6@RestController
7public class TestController {
8
9 @GetMapping("/test")
10 public String test() {
11 return "requestId: %s, userId: %s".formatted(
12 RequestContext.getRequestId(), RequestContext.getUserId());
13 }
14}
测试
1curl http://localhost:8080/test
2requestId: bf713edb-1a84-488f-9481-6be54ef9e8f3, userId: user-1234
不用 ThreadLocal 的方式,比如任何时候要在 HTTP 线程上找 Request 的相关数据就不必从 RequestContextHolder.getRequestAttributes()
中翻找。
Scoped Values 像 Rust 那样的绑定
Scopped Values 的值绑定就点像 Rust 的规则一样
1 let name = "value1";
2 println!("{}", name); // value1
3 {
4 let name = "value2"; // shadowing, 遮盖了外部的 name
5 println!("{}", name); // value2
6 }
7 println!("{}", name); // value1
在 Rust 中 let 被称作值的绑定,块内部的绑定临时遮盖外部绑定的值,Java 的 Scoped Values 也有类似的效果
1 ScopedValue.where(VALUE, "value1").run(() -> {
2 System.out.println(VALUE.get()); // value1
3 ScopedValue.where(VALUE, "value2").run(() -> {
4 System.out.println(VALUE.get()); // value2
5 });
6 System.out.println(VALUE.get()); //value1
7 });
Scoped Values 在实现上也确实如其名所表达的那样,在每个 where(X, v).run(()->...) 时就会开启一个 Scope 并在该 Scope 上绑定值,
再来 where(Y, w).run(()->...) 又会往下层创建一个 Scope, 在 Scope 中要使用某个值时则从内往外找,找到即止。
子线程上获取 Scoped Values
InteritableThreadLocal 会让子线程自动继承父线程上绑定的值,但必须父线程上有绑定值后用 new Thread() 创建的线程(包括线程池中新创建的线程)
才能继承父线程上绑定的值。
前面提过这个疑问,那么用 Scoped Values 如何让子线程共享父线程上绑定的数据呢?最好的实现方式是在创建虚拟线程时用结构化并发(Structured
Concurrency API(JEP 505)), 使用类 StructuredTaskScope, 父线程上绑定的值会通过 StructuredTaskScope 自动被子虚拟线程继承,
而且这其中还不存在 Scoped Values 的拷贝过程。
代码演示
1 ScopedValue.where(VALUE, "123").run(() -> {
2 try (var scope = StructuredTaskScope.open()) {
3 scope.fork(() -> {
4 System.out.printf("%s: %s: %s\n", "task1", Thread.currentThread(), VALUE.get());
5 });
6 scope.fork(() -> {
7 System.out.printf("%s: %s: %s\n", "task2", Thread.currentThread(), VALUE.get());
8 });
9 scope.join();
10 } catch (InterruptedException e) {
11 throw new RuntimeException(e);
12 }
13 });
StructuredTaskScope 在 Java 25 和 26 中仍处于预览阶段,scope.join() 后绑定的值会被自动清除。StructuredTaskScope 还不能与传统
的线程池(像 FokJoinPool)一起使用,这是一个缺憾,只能用于虚拟子线程。
上面代码的输出结果为
1task1: VirtualThread[#28]/runnable@ForkJoinPool-1-worker-1: 123
2task2: VirtualThread[#30]/runnable@ForkJoinPool-1-worker-2: 123
值是传递到了子虚拟线程去了。
Scoped Values 真的是不可变的吗?
Scoped Values 的 Immutable 只是说一旦向线程绑定了值后,在相同的范围内不能像 ThreadLocal.set(value) 那样可以重新绑定值,只能通过
ScopedValue.where(key, value) 来绑定并生成一个 Scope, 如果绑定的值的内部状态是可变的,执行下方的代码
1public class Test {
2 static final ScopedValue<User> VALUE = ScopedValue.newInstance();
3
4 public static void main(String[] args) {
5 User user = new User("name1");
6 ScopedValue.where(VALUE, user).run(() -> {
7 User u = VALUE.get();
8 System.out.println(u.name); // name1
9 u.name = "name2";
10 System.out.println(VALUE.get().name); // name2
11 });
12 System.out.println(user.name); // name2
13 }
14
15 static class User {
16 public String name;
17
18 public User(String name) {
19 this.name = name;
20 }
21 }
22}
输出值分别为
name1
name2
name2
迁移到 Scoped Values
在我们使用 ThreadLocal 时应首先考虑一下是否可以用 Scoped Values 实现。Scoped Values 能自动检测递归,可用于嵌套事物当中,
比如当前已存在一个事物则当前操作自动加入到该事物当中,Spring 的事务就是靠 Aspect 和 ThreadLocal 实现的。
Scoped Values 在一个范围中绑定多个值可以把多个 where() 串联起来,如
1 ScopedValue<String> VALUE1 = ScopedValue.newInstance();
2 ScopedValue<String> VALUE2 = ScopedValue.newInstance();
3 ScopedValue.where(VALUE1, "111").where(VALUE2, "222").run(() -> {
4 System.out.println(VALUE1.get()); // 111
5 System.out.println(VALUE2.get()); // 222
6 });
以后还要继续关注 structured concurrency 的发展,不知到后面 Scoped Values 能不能支持平台线程,以及传统的线程池。
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。