首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >《Java 演进之路》系列 · 第 16 篇

《Java 演进之路》系列 · 第 16 篇

作者头像
DevLlama
发布2026-06-01 20:16:49
发布2026-06-01 20:16:49
880
举报

Scoped Values 来了:JDK 20 如何让线程局部变量更优雅

ThreadLocal 虽好,但用起来总像在走钢丝;Scoped Values 则像是给它装上了安全护栏

2023年3月,JDK 20正式发布。和JDK 18、19一样,它不是一个LTS版本,生命周期只有半年。但它带来的一个预览特性——Scoped Values(作用域值),却让我眼前一亮。这个特性旨在提供一种比 ThreadLocal 更安全、更高效的方式来在线程内共享不可变数据。与此同时,虚拟线程和结构化并发也迎来了它们的第二次预览,API变得更加稳定。JDK 20的目标很务实:在JDK 19的基础上,继续打磨那些可能改变未来的“种子”特性

JDK 20 官方特性总览

先提一句官方背景哈:JDK 20的所有新特性都是基于 JSR 395(《Java SE 20 发布规范》) 来实现的。作为一个标准的短期版本,它的改进策略非常聚焦:

  • • 第一块是Project Loom 特性的持续迭代:虚拟线程和结构化并发进入第二次预览,Scoped Values 首次亮相。
  • • 第二块是关键孵化器项目的推进:Foreign Function & Memory API 进入第四次孵化,Vector API 也迎来了第五轮。
  • • 第三块是对开发者体验的微调:比如系统属性中的进程 ID,以及对字符串拼接的字节码优化。

整体来看,JDK 20没有引入颠覆性的新概念,而是专注于深化和优化,让那些有潜力的特性离生产环境更近一步。

1. Scoped Values(预览版,JEP 429)

这是我最想聊聊的特性。长久以来,ThreadLocal 是我们在同一线程内传递上下文(比如用户信息、追踪ID)的首选工具。但它有几个让人头疼的问题:

  • 内存泄漏风险:如果忘记 remove(),对象会一直被持有,尤其是在使用线程池时。
  • 可变性ThreadLocal 里的值是可变的,这在并发环境下容易引发意外的bug。
  • 与虚拟线程不搭:虚拟线程是轻量级且大量创建的,为每个虚拟线程维护一个 ThreadLocal 映射表,开销太大。

Scoped Values 就是为了解决这些问题而生的。它的核心思想是:将不可变的数据绑定到一个特定的代码作用域上,并且只对当前线程及其子线程可见

说下我的理解:你可以把它想象成一个“一次写入、到处读取”的上下文容器。你通过 where(key, value).run(() -> {...}) 的方式设置一个值,然后在这个 run 方法内部的任何地方,都可以通过 key.get() 来安全地读取它。

代码语言:javascript
复制
// 定义作用域值
static final ScopedValue<String> TRACE_ID = ScopedValue.newInstance();

public static void main(String[] args) {
    // 绑定作用域值并执行逻辑
    ScopedValue.where(TRACE_ID, "trace-123456")
              .run(() -> {
                  doBusinessLogic();
              });
    // 作用域结束,TRACE_ID 自动失效,无内存泄漏风险
}

private static void doBusinessLogic() {
    // 任意嵌套层级都能读取
    System.out.println("当前追踪ID:" + TRACE_ID.get());
    callSubService();
}

private static void callSubService() {
    // 子方法中同样可读取
    System.out.println("子服务追踪ID:" + TRACE_ID.get());
}

我的初体验:我写了一个简单的日志追踪示例。以前用 ThreadLocal,每次请求结束都得记得清理。现在用 Scoped Values,作用域一结束,值就自动消失了,完全不用操心内存泄漏。 • 最大的感受它和结构化并发简直是绝配!在 StructuredTaskScope 里启动的子任务,可以自动继承父作用域里的 Scoped Values。这意味着,你可以在整个并发任务树中安全地传递上下文,而不用担心数据污染或泄漏。

对我来说,Scoped Values 不是一个炫酷的新玩具,而是一个能真正解决实际痛点的工具。它让线程内数据共享这件事,变得既安全又简单。

2. 虚拟线程(第二次预览,JEP 436)

虚拟线程在 JDK 19 首次亮相后,这次迎来了第二次预览。主要变化是 API 更稳定了,一些边缘情况的处理也更完善了。

代码语言:javascript
复制
// JDK 20 虚拟线程使用示例(需 --enable-preview)
public class VirtualThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        // 虚拟线程执行器,任务结束自动回收
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            // 提交10000个I/O密集型任务
            for (int i = 0; i < 10000; i++) {
                int taskId = i;
                executor.submit(() -> {
                    // 模拟数据库查询等阻塞I/O
                    try {
                        Thread.sleep(100);
                        System.out.println("虚拟线程执行任务:" + taskId);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            }
        } // 自动关闭执行器,等待所有虚拟线程完成
    }
}

我重新跑了一遍 JDK 19 里的压测例子,结果和之前差不多:用虚拟线程轻松扛住上万并发,而代码依然是同步阻塞的写法。最大的不同是,这次我对 synchronized 块的行为有了更深的理解——在虚拟线程中,进入 synchronized 块可能会导致载体线程被“钉住”(pinned),从而影响吞吐量。官方建议尽量使用 ReentrantLock 等显式锁。

总的来说,虚拟线程正在稳步走向成熟。虽然离生产环境还有一段路,但它的潜力已经毋庸置疑。

3. 结构化并发(第二次孵化,JEP 437)

和虚拟线程一样,结构化并发也进入了第二轮。这次的主要改进是简化了 API,并增强了与 Scoped Values 的集成。

代码语言:javascript
复制
// JDK 20 结构化并发示例(需 --enable-preview)
public class StructuredConcurrencyDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            // 提交两个子任务
            Future<String> userTask = scope.fork(() -> fetchUser("1001"));
            Future<List<String>> orderTask = scope.fork(() -> fetchOrders("1001"));

            scope.join(); // 等待所有子任务完成
            scope.throwIfFailed(); // 任一任务失败则抛出异常

            // 安全获取结果
            String user = userTask.resultNow();
            List<String> orders = orderTask.resultNow();
            System.out.println("用户:" + user + ",订单:" + orders);
        }
    }

    private static String fetchUser(String userId) {
        // 模拟RPC调用
        return "用户-" + userId;
    }

    private static List<String> fetchOrders(String userId) {
        // 模拟数据库查询
        return List.of("订单1", "订单2");
    }
}

我试着把之前的批量查询工具用新的 API 重写了一遍,代码逻辑几乎没有变化,但感觉更“顺”了。特别是错误处理部分,ShutdownOnFailure 策略工作得非常可靠——只要有一个子任务失败,其他子任务会立即被取消,避免了资源浪费和“孤儿线程”问题。

4. Foreign Function & Memory API(第四孵化器,JEP 434)

FFM API 又双叒叕来了!这是它的第四次孵化。这次的改动主要是为了提升易用性和性能。

代码语言:javascript
复制
// JDK 20 调用C语言strlen函数示例(需 --enable-preview)
public class FfmApiDemo {
    public static void main(String[] args) {
        // 加载C标准库
        try (var linker = Linker.nativeLinker()) {
            // 获取strlen函数句柄
            MethodHandle strlen = linker.downcallHandle(
                linker.defaultLookup().find("strlen").get(),
                FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
            );

            // 分配内存并写入字符串
            try (var arena = Arena.ofConfined()) {
                MemorySegment str = arena.allocateFrom("Hello JDK 20");
                // 调用strlen
                long len = (long) strlen.invokeExact(str.address());
                System.out.println("字符串长度:" + len); // 输出 11
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

我再次尝试调用 C 的 strlen 函数,发现代码比 JDK 19 时又简洁了一些。最关键的是,这次它终于提供了更好的错误提示,当我传错参数类型时,不再是一堆晦涩的 JVM 错误,而是清晰的 Java 异常。

虽然我还是觉得它有点复杂,但能明显感觉到 OpenJDK 社区在努力让它变得对普通开发者更友好。JNI 的替代品,真的越来越近了。

🧩 为什么关注 JDK 20?

  • Scoped Values 的引入:为线程内数据共享提供了一种更安全、更现代的解决方案,有望成为 ThreadLocal 的继任者。
  • Loom 特性的持续成熟:虚拟线程和结构化并发的第二次预览,表明它们正朝着稳定和生产就绪的方向稳步前进。
  • 短期版本的价值体现:JDK 20 完美展示了短期版本如何作为创新特性的“试验田”,通过快速迭代收集反馈。
  • 生态的稳步推进:FFM API 和 Vector API 的持续孵化,保证了 Java 在系统编程和高性能计算领域的竞争力。

🧱 其他重要平台增强

Vector API (第五孵化器, JEP 438):又一轮孵化!这次的重点是进一步提升跨平台兼容性和性能。我在 M1 上试了试,虽然还是有些小问题,但至少能跑通简单的矩阵运算例子了。它让 Java 能更高效地利用 CPU 向量指令,在数值计算场景下性能提升明显。

系统属性中的进程 ID (JEP 426):现在可以通过 ProcessHandle.current().pid() 或新的系统属性 jdk.process.pid 直接获取当前 Java 进程的 PID。这在编写运维脚本或诊断工具时非常方便,再也不用去解析 jps 的输出了。

代码语言:javascript
复制
// 获取当前进程PID
long pid = ProcessHandle.current().pid();
System.out.println("当前进程ID:" + pid);
// 或通过系统属性
String pidStr = System.getProperty("jdk.process.pid");

字节码的字符串拼接优化 (JEP 430):这是一个底层优化,对我们写代码几乎没影响,但能让字符串拼接操作(尤其是包含多个变量的 + 操作)变得更高效。编译器会生成更紧凑的字节码,减少临时对象创建,默默无闻,但功不可没。

✅ 总结

JDK 20通过JSR 395,完成了一次稳健而专注的迭代。它没有追求宏大的叙事,而是将精力集中在几个关键的前沿特性上:Scoped Values 的引入为上下文传递带来了新思路,虚拟线程与结构化并发的二次预览则让 Project Loom 的愿景更加清晰。作为一个短期版本,JDK 20出色地扮演了“承前启后”的角色,为这些高潜力特性通往最终的 LTS 版本铺平了道路。

📢 延伸阅读建议

  • • JSR 395: Java SE 20 规范[1]
  • • OpenJDK JDK 20 官方页面[2]
  • • JEP 429: Scoped Values (Preview)[3]
  • • JEP 436: Virtual Threads (Second Preview)[4]
  • • JEP 437: Structured Concurrency (Second Incubator)[5]
  • • JEP 434: Foreign Function & Memory API (Fourth Incubator)[6]
引用链接

[1] JSR 395: Java SE 20 规范: https://www.jcp.org/en/jsr/detail?id=395 [2] OpenJDK JDK 20 官方页面: https://openjdk.org/projects/jdk/20/ [3] JEP 429: Scoped Values (Preview): https://openjdk.org/jeps/429 [4] JEP 436: Virtual Threads (Second Preview): https://openjdk.org/jeps/436 [5] JEP 437: Structured Concurrency (Second Incubator): https://openjdk.org/jeps/437 [6] JEP 434: Foreign Function & Memory API (Fourth Incubator): https://openjdk.org/jeps/434

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-03-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DevLlama 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Scoped Values 来了:JDK 20 如何让线程局部变量更优雅
  • JDK 20 官方特性总览
    • 1. Scoped Values(预览版,JEP 429)
    • 2. 虚拟线程(第二次预览,JEP 436)
    • 3. 结构化并发(第二次孵化,JEP 437)
    • 4. Foreign Function & Memory API(第四孵化器,JEP 434)
  • 🧩 为什么关注 JDK 20?
  • 🧱 其他重要平台增强
  • ✅ 总结
  • 📢 延伸阅读建议
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档