ThreadLocal虽好,但用起来总像在走钢丝;Scoped Values 则像是给它装上了安全护栏。
2023年3月,JDK 20正式发布。和JDK 18、19一样,它不是一个LTS版本,生命周期只有半年。但它带来的一个预览特性——Scoped Values(作用域值),却让我眼前一亮。这个特性旨在提供一种比 ThreadLocal 更安全、更高效的方式来在线程内共享不可变数据。与此同时,虚拟线程和结构化并发也迎来了它们的第二次预览,API变得更加稳定。JDK 20的目标很务实:在JDK 19的基础上,继续打磨那些可能改变未来的“种子”特性。
先提一句官方背景哈:JDK 20的所有新特性都是基于 JSR 395(《Java SE 20 发布规范》) 来实现的。作为一个标准的短期版本,它的改进策略非常聚焦:
整体来看,JDK 20没有引入颠覆性的新概念,而是专注于深化和优化,让那些有潜力的特性离生产环境更近一步。
这是我最想聊聊的特性。长久以来,ThreadLocal 是我们在同一线程内传递上下文(比如用户信息、追踪ID)的首选工具。但它有几个让人头疼的问题:
remove(),对象会一直被持有,尤其是在使用线程池时。ThreadLocal 里的值是可变的,这在并发环境下容易引发意外的bug。ThreadLocal 映射表,开销太大。Scoped Values 就是为了解决这些问题而生的。它的核心思想是:将不可变的数据绑定到一个特定的代码作用域上,并且只对当前线程及其子线程可见。
说下我的理解:你可以把它想象成一个“一次写入、到处读取”的上下文容器。你通过 where(key, value).run(() -> {...}) 的方式设置一个值,然后在这个 run 方法内部的任何地方,都可以通过 key.get() 来安全地读取它。
// 定义作用域值
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 不是一个炫酷的新玩具,而是一个能真正解决实际痛点的工具。它让线程内数据共享这件事,变得既安全又简单。
虚拟线程在 JDK 19 首次亮相后,这次迎来了第二次预览。主要变化是 API 更稳定了,一些边缘情况的处理也更完善了。
// 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 等显式锁。
总的来说,虚拟线程正在稳步走向成熟。虽然离生产环境还有一段路,但它的潜力已经毋庸置疑。
和虚拟线程一样,结构化并发也进入了第二轮。这次的主要改进是简化了 API,并增强了与 Scoped Values 的集成。
// 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 策略工作得非常可靠——只要有一个子任务失败,其他子任务会立即被取消,避免了资源浪费和“孤儿线程”问题。
FFM API 又双叒叕来了!这是它的第四次孵化。这次的改动主要是为了提升易用性和性能。
// 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 的替代品,真的越来越近了。
ThreadLocal 的继任者。• Vector API (第五孵化器, JEP 438):又一轮孵化!这次的重点是进一步提升跨平台兼容性和性能。我在 M1 上试了试,虽然还是有些小问题,但至少能跑通简单的矩阵运算例子了。它让 Java 能更高效地利用 CPU 向量指令,在数值计算场景下性能提升明显。
• 系统属性中的进程 ID (JEP 426):现在可以通过 ProcessHandle.current().pid() 或新的系统属性 jdk.process.pid 直接获取当前 Java 进程的 PID。这在编写运维脚本或诊断工具时非常方便,再也不用去解析 jps 的输出了。
// 获取当前进程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 版本铺平了道路。
[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