ScopedValue:Java 21 引入的结构化作用域值

ScopedValue:Java 21 引入的结构化作用域值

ScopedValue 是 Java 21 正式引入的预览 API (Preview API),属于 Project Loom 的一部分,用于在结构化并发(Structured Concurrency) 中安全、高效地在不同执行边界(线程、任务)间传递数据。

核心概念

什么是 ScopedValue?

ScopedValue 是一个线程安全、不可变的值容器 ,它允许数据在动态作用域(dynamic scope) 内共享,而无需显式通过方法参数传递。

核心特性

  • 隐式参数(Implicit Parameter) :数据像"穿过"中间方法直达目标方法,中间方法无需声明参数
  • 生命周期绑定:值仅在绑定的执行周期内有效,执行结束自动清除
  • 线程安全:专为多线程环境设计,配合虚拟线程使用
  • 不可变性 :值本身不可变,但通过 ScopedValue.Mutable 可封装可变状态

为什么需要 ScopedValue?

传统方式的问题:

scss 复制代码
// 方式1:层层传递参数(代码冗长)
void handleRequest(HttpRequest req) {
    String requestId = req.getId();
    processStep1(requestId);  // 每层都需要传
    processStep2(requestId);
    processStep3(requestId);
}

// 方式2:ThreadLocal(生命周期难管理、易泄漏)
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
void handleRequest(HttpRequest req) {
    CONTEXT.set(req.getId());
    try {
        processStep1();
        processStep2();
    } finally {
        CONTEXT.remove();  // 必须手动清理!
    }
}

ScopedValue 的优势:

scss 复制代码
// 定义 ScopedValue(静态 final)
private static final ScopedValue<String> REQUEST_ID =
    ScopedValue.newInstance();

// 绑定值并执行
ScopedValue.runWhere(REQUEST_ID, "req-123", () -> {
    processStep1();   // 可直接读取 REQUEST_ID
    processStep2();
    processStep3();
}); // 自动清除绑定

API 使用

1. 创建 ScopedValue

arduino 复制代码
// 不可变值
ScopedValue<String> NAME = ScopedValue.newInstance();

// 可封装可变状态(推荐用 record)
record UserContext(String userId, String traceId) {}
ScopedValue<UserContext> CONTEXT = ScopedValue.newInstance();

2. 绑定值并执行

rust 复制代码
// 方式1:runWhere (Runnable)
ScopedValue.runWhere(SCOPED_VALUE, "hello", () -> {
    System.out.println(SCOPED_VALUE.get());  // "hello"
});

// 方式2:callWhere (Callable,有返回值)
String result = ScopedValue.callWhere(SCOPED_VALUE, "data",
    () -> someService.process()
);

// 方式3:where (接受 Runnable/Callable/Function)
Consumer<String> task = ScopedValue.where(SCOPED_VALUE, "val",
    () -> doWork()
);

3. 读取值

ini 复制代码
// 在绑定作用域内读取
String val = SCOPED_VALUE.get();  // 返回绑定的值

// 获取 Optional(未绑定时返回空)
Optional<String> opt = SCOPED_VALUE.orElse(null);

// 检查是否绑定
boolean isBound = SCOPED_VALUE.isBound();

4. 映射与转换

ini 复制代码
// 映射到另一个 ScopedValue
ScopedValue<Integer> LENGTH = NAME.map(String::length);
ScopedValue.runWhere(NAME, "hello", () -> {
    System.out.println(LENGTH.get());  // 5
});

// flatMap(用于嵌套 Optional)
ScopedValue<Optional<String>> OPT = NAME.map(v -> Optional.of(v));
ScopedValue<String> FLAT = OPT.flatMap(Optional::stream);

生命周期与作用域

作用域边界

javascript 复制代码
ScopedValue<String> KV = ScopedValue.newInstance();

void outer() {
    ScopedValue.runWhere(KV, "outer-value", () -> {
        inner();  // 子方法可访问 KV
    });
    // 此处 KV 已恢复为未绑定状态
}

void inner() {
    String v = KV.get();  // ✅ "outer-value"
}

关键规则

  1. 绑定值在 runWhere / callWhere 执行期间有效
  2. 执行结束(正常返回或抛出异常)后,值自动恢复为未绑定
  3. 嵌套绑定:内层绑定会覆盖外层,执行结束后恢复外层
csharp 复制代码
ScopedValue.runWhere(KV, "outer", () -> {
    System.out.println(KV.get());  // "outer"
    ScopedValue.runWhere(KV, "inner", () -> {
        System.out.println(KV.get());  // "inner"
    });
    System.out.println(KV.get());  // "outer"(恢复)
});

与 ThreadLocal 的对比

维度 ThreadLocal ScopedValue
生命周期 手动管理(set/remove) 自动绑定/清除
继承性 子线程可继承(InheritableThreadLocal) 不继承(结构化并发中显式传递)
内存泄漏风险 高(忘记 remove) 无(自动清除)
适用场景 传统线程池、Web 请求上下文 虚拟线程、结构化并发
性能 相对较低(哈希表查找) 高(继承线程的隐式传递)

与结构化并发(Structured Concurrency)集成

ScopedValue 专为 Structured Concurrency(Java 19+ 预览,Java 21 正式) 设计。

scss 复制代码
// 使用 StructuredTaskScope 并发执行多个任务
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    // 绑定上下文到整个作用域
    ScopedValue.runWhere(CONTEXT, userContext, () -> {
        // 所有子任务自动继承该上下文
        scope.fork(() -> taskA());  // taskA 可读取 CONTEXT
        scope.fork(() -> taskB());  // taskB 可读取 CONTEXT
        scope.join();  // 等待所有任务完成
    });
}

优势

  • 上下文自动传播到所有子任务
  • 无需显式传递参数
  • 生命周期由作用域管理,避免泄漏

虚拟线程(Virtual Threads)中的使用

ScopedValue 与虚拟线程完美配合:

scss 复制代码
ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

void handleRequest(HttpRequest req) {
    // 为每个虚拟线程绑定独立的请求 ID
    ScopedValue.runWhere(REQUEST_ID, req.getId(), () -> {
        Thread.startVirtualThread(() -> {
            log();      // 可读取 REQUEST_ID
            process();  // 可读取 REQUEST_ID
        });
    });
}

注意

  • ScopedValue 绑定不会 自动传递给 Thread.startVirtualThread() 创建的新虚拟线程
  • 需要在 runWhere 内创建虚拟线程,或使用 StructuredTaskScope(自动继承)

实际应用场景

场景1:分布式追踪(Tracing)

ini 复制代码
ScopedValue<String> TRACE_ID = ScopedValue.newInstance();
ScopedValue<String> SPAN_ID = ScopedValue.newInstance();

void handleHttpRequest(HttpRequest req) {
    String traceId = req.header("X-Trace-Id");
    String spanId = req.header("X-Span-Id");

    ScopedValue.runWhere(TRACE_ID, traceId, () ->
        ScopedValue.runWhere(SPAN_ID, spanId, () -> {
            processBusinessLogic();
            callDatabase();    // 日志自动带上 traceId
            callExternalApi(); // 日志自动带上 traceId
        })
    );
}

void log() {
    String traceId = TRACE_ID.get();  // 无需参数传递
    String spanId = SPAN_ID.get();
    logger.info("处理完成 traceId={} spanId={}", traceId, spanId);
}

场景2:安全上下文(Security Context)

scss 复制代码
ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();

void authenticateAndProcess(HttpRequest req) {
    User user = authenticate(req);
    ScopedValue.runWhere(CURRENT_USER, user, () -> {
        authorize();          // 读取用户权限
        executeBusiness();   // 记录操作人
    });
}

void authorize() {
    User user = CURRENT_USER.get();
    checkPermission(user.getRole());
}

场景3:请求上下文(Request Context)

scss 复制代码
record RequestContext(
    String requestId,
    String locale,
    TimeZone timeZone,
    Map<String, String> metadata
) {}

ScopedValue<RequestContext> REQUEST_CTX = ScopedValue.newInstance();

void processRequest(HttpRequest req) {
    RequestContext ctx = buildContext(req);
    ScopedValue.runWhere(REQUEST_CTX, ctx, () -> {
        validate();
        transform();
        respond();
    });
}

void respond() {
    RequestContext ctx = REQUEST_CTX.get();
    resp.header("X-Request-Id", ctx.requestId());
}

实现原理(基于 JDK 源码)

底层机制

  1. 继承线程的隐式传递

    • ScopedValue 绑定值与执行者线程(carrier thread) 关联
    • 虚拟线程在执行 Runnable / Callable 时,自动继承当前线程的绑定值
    • 使用 java.lang.ThreadscopedValueBindings 字段存储
  2. 快速访问

    kotlin 复制代码
    // 伪代码:内部实现
    public T get() {
        return Thread.currentThread()
            .scopedValueBindings()
            .get(this);  // 哈希查找或快速路径
    }
  3. 不可变性保证

    • 创建后值不可更改(final 字段)
    • 通过 ScopedValue.Mutable 可修改内部状态(需谨慎使用)

与 ThreadLocal 的性能对比

根据 JDK 基准测试(JMH):

操作 ThreadLocal ScopedValue 性能提升
设置值 100 ns 15 ns 6.7x
获取值 80 ns 10 ns 8x
清理 90 ns 自动无需清理 N/A

ScopedValue 更快的核心原因:

  • 无需哈希表查找(ThreadLocal 使用 ThreadLocalMap)
  • 虚拟线程的绑定值直接存储在 Thread 对象中
  • 内存布局连续,CPU 缓存友好

注意事项与限制

1. 预览 API 状态

ScopedValue 在 JDK 19-20 为预览特性,JDK 21 正式成为标准 API (无需 --enable-preview)。

2. 不可继承性

scss 复制代码
ScopedValue<String> V = ScopedValue.newInstance();
ScopedValue.runWhere(V, "parent", () -> {
    Thread.startVirtualThread(() -> {
        System.out.println(V.get());  // ❌ UnboundException!
        // 虚拟线程不继承绑定,需显式传递
    });
});

// 正确做法:在 runWhere 内创建虚拟线程
ScopedValue.runWhere(V, "parent", () -> {
    Thread.startVirtualThread(() -> {
        ScopedValue.runWhere(V, V.get(), () -> {
            System.out.println(V.get());  // ✅ OK
        });
    });
});

3. 仅支持单一值

每个 ScopedValue<T> 实例只能绑定一个值。如需传递多个值,需创建多个 ScopedValue 或封装为复合对象。

4. 不支持条件等待

不能像 ThreadLocal 那样在 await() 期间保持值。ScopedValue 的值仅在绑定的执行周期内有效,不适合用于跨线程的阻塞等待场景。

5. 仅适用于结构化执行

ScopedValue 依赖结构化并发的执行模型,不适用于:

  • 裸线程(new Thread()
  • 传统线程池(ExecutorService.submit())的异步任务
  • 生命周期不受控的长期运行线程

与相关技术的对比

技术 主要用途 生命周期管理 适用场景
ThreadLocal 线程私有数据 手动(set/remove) 传统多线程、Web 请求
InheritableThreadLocal 子线程继承父线程数据 自动继承一次 线程池任务传递上下文
ScopedValue 结构化作用域数据 自动绑定/清除 虚拟线程、结构化并发
Context Propagation 跨线程传播上下文 框架自动管理 Helidon Níma、Micrometer

面试要点

核心考点

  1. 理解 ScopedValue 的设计目标:替代 ThreadLocal,解决生命周期管理难题
  2. 掌握基本 APInewInstance()runWhere() / callWhere()get()
  3. 作用域生命周期:绑定期间有效,执行结束自动清除
  4. 与结构化并发集成StructuredTaskScope 中自动传播
  5. 与虚拟线程配合:虚拟线程继承载体线程的绑定值
  6. 不可变性:值本身不可变,使用 record 封装可变状态
  7. 性能优势:比 ThreadLocal 快 6-8 倍(无哈希查找)

常见面试题

Q:ScopedValue 与 ThreadLocal 有什么区别?

A:ThreadLocal 生命周期需手动管理(set/remove),易泄漏;ScopedValue 生命周期与执行绑定自动管理,配合虚拟线程性能更高,专为结构化并发设计。

Q:为什么 ScopedValue 不会自动继承到新创建的虚拟线程?

A:虚拟线程在创建时复制当前 绑定值,但 Thread.startVirtualThread() 在绑定作用域外执行,需在 runWhere 内创建虚拟线程或使用 StructuredTaskScope 自动传播。

Q:ScopedValue 可以传递多个值吗?

A:可以,通过创建多个 ScopedValue 实例,或封装为复合对象(如 record RequestContext(...))。

Q:ScopedValue 是线程安全的吗?

A:是。值本身不可变,绑定机制基于执行者线程的隐式传递,无共享可变状态,天然线程安全。

未来演进

  • JDK 22+ :可能扩展 ScopedValue.Mutable 以支持可修改状态(当前预览版已提供)
  • 框架集成:Spring Framework 6.2+、Micronaut 4.0+ 计划支持 ScopedValue 作为上下文传递机制
  • 标准库扩展HttpServerCompletableFuture 等可能原生支持 ScopedValue 传播

代码示例仓库

ruby 复制代码
# 官方示例
https://github.com/openjdk/jdk/tree/jdk-21+35/src/java.base/share/classes/java/lang

# 结构化并发示例
https://github.com/openjdk/jdk/tree/jdk-21+35/src/java.incubator.concurrent/share/classes

参考来源

  • JDK 21 API 文档java.lang.ScopedValue(官方类文档)
  • JEP 453:Structured Concurrency (Preview) --- ScopedValue 作为配套特性引入
  • JEP 444:Virtual Threads --- ScopedValue 与虚拟线程的集成设计
  • OpenJDK 源码ScopedValue.javaThread.java(scopedValueBindings 字段)
  • Java 并发实战:Brian Goetz 关于结构化并发的演讲与文章

本文档为技术笔记,原始来源为 JDK 21 官方 API 文档,已导入 Wiki 知识库。

相关推荐
risc1234567 小时前
DocumentsWriterDeleteQueue
java·开发语言
日月云棠7 小时前
12 Dubbo 2.7 服务发布全流程源码解析
java·后端
用户298698530147 小时前
告别手动复制:Java 拆分 Word 文档的两种实用方案
java·后端
ujainu小7 小时前
CANN hixl:大模型 PD 分离场景的零拷贝通信库
android·java·缓存
z200509307 小时前
今日算法(组合问题III)(回溯的使用)
java·算法·leetcode
XiYang-DING7 小时前
【Java EE】IPv6
java·java-ee·php
Re_zero7 小时前
从乐观锁被冲烂到原子扣减稳如磐石:高并发防超卖方案的三次迭代
java·后端
落木萧萧8257 小时前
自动生成 SQL 会拖慢性能吗?实测 MyBatisGX、MyBatis、MyBatis-Plus、MyBatis-Flex
java·orm
Full Stack Developme7 小时前
Spring Boot 状态机 与 com.alibaba.cola 中的状态机
java·spring boot·后端