JDK 22/23新特性盘点:虚拟线程、FFM API与结构化并发实战

发布时间:2026/10/14 21:56:51
JDK 22/23新特性盘点:虚拟线程、FFM API与结构化并发实战 说实话写这个系列到第三章的时候我已经不知道该用什么标题才能把范围框住。前两章把 Java 16 到 Java 21 那批最显眼的特性——记录类型、密封类、模式匹配、虚拟线程——拆得差不多了剩下的大多是“听过名字但不常用”的边角料。可真把这章需要的材料翻完 2024 年的 JDK 22 和 JDK 23 的更新清单之后我反而觉得这是整个系列里最有价值的一章虚拟线程落地之后并发模型该怎么跟着调整字符串模板预览了三次为什么还没定稿JNI 这个老伙计是不是真到了退休边缘这些问题直接决定了你明年要不要升级 JDK、现有代码要动多少。这篇文章就按我自己的关注顺序把这些在 2024 年已经转正或者快要转正的能力挨个过一遍。适合整天跟并发、字节码、底层互操作打交道的人也适合想年底评估技术栈、但没时间一份份翻 JEP 的人。1. 并发这块拼图虚拟线程之后线程本地变量反而成了新瓶颈1.1 虚拟线程带来的隐性成本Java 21 把虚拟线程转正之后很多团队的第一反应是把线程池换掉连接数从几百拉到几千几万压测曲线确实好看。但用得深了就会发现一个之前不起眼的问题ThreadLocal。传统线程池里线程数量有限每个线程上挂几个ThreadLocal对象内存开销还能接受。换成虚拟线程后线程是按任务数创建的一个高并发服务里同时存活几万甚至几十万个虚拟线程很正常每个虚拟线程如果都带一份完整的请求上下文副本光这些本地变量占的内存就很可观。更麻烦的是语义问题。ThreadLocal本身是可变容器子线程不会自动继承父线程的值就算用InheritableThreadLocal继承到的也是一份引用你再往里塞个可变对象父线程改了子线程随时能看到。对于“一次请求内只读、希望整条调用链共享”的上下文数据这个设计既重又不安全。1.2 StructuredTaskScope把“分叉-聚合”做成标准动作真正的解法不是继续在ThreadLocal上打补丁而是换一套并发模型。StructuredTaskScope在 Java 21 正式落地JEP 453解决的是我前面说的那个痛点任务结构化地嵌套每个子任务的生命周期都被限制在父任务的代码块里退出时要么全部成功要么统一收尾。拿最常见的“并行查两个数据源再聚合”举例try (var scope new StructuredTaskScope.ShutdownOnFailure()) { FutureUserInfo userFuture scope.fork(() - queryUser(id)); FutureListOrder orderFuture scope.fork(() - queryOrders(id)); scope.join(); // 等待所有子任务结束或被关闭 scope.throwIfFailed(); // 任何一个子任务抛出异常这里统一抛出来 return new Profile(userFuture.resultNow(), orderFuture.resultNow()); }写一遍就能体会到和ExecutorService的核心差异scope.join()保证代码块退出前所有子任务都结束try-with-resources 保证泄漏的任务会被强制干预throwIfFailed()让异常处理不再依赖每个Future.get()里重复检查。对比以前手动submit、手动get、忘记shutdown的写法这几乎是在语法层面逼你写出不会漏任务的代码。我实际用下来还有个小技巧如果多个子任务里有共同的外部资源千万别在任务里自己开连接、自己关连接。把资源获取也作为子任务放进同一个 scope由父任务统一管理生命周期比在子任务里各扫门前雪稳得多。1.3 ScopedValue线程本地变量的可预期替代Scoped Values走的是另一条路它从设计上就是“不可变、单次赋值、按作用域传递”的上下文容器。现在它还在预览阶段Java 22 的 JEP 464、Java 23 的 JEP 481但 API 形态已经很清晰了。private static final ScopedValueString REQ_ID ScopedValue.newInstance(); void handle(Request req) { ScopedValue.where(REQ_ID, req.id()) .run(() - doWork(req)); } void doWork(Request req) { // 不需要把 req.id 层层传递这里直接读作用域内的值 System.out.println(REQ_ID.get()); scope.fork(() - childTask()); }where(...).run(...)划定了一段动态作用域在这段代码以及它派生的任务里REQ_ID.get()拿到的都是同一个固定值。和ThreadLocal最大的不同是值不可变你在子任务里看到的永远是最外层绑定的那个值不会被中途改写同时它传递的只是绑定关系不是把一个大对象在每个线程里复制一遍。需要注意的点在文档里写得很含蓄实际踩坑才会意识到作用域外调用get()返回的是null不是抛异常。所以生产代码里要么在绑定时就用getOrThrow()这类方法要么提前判空否则线上会出现一批“为什么有时候是 null”的诡异问题。另外它现在还不能摆脱--enable-preview想上生产环境前务必确认 JDK 版本里这个特性是否已经转正。我自己对这三者的选择很明确请求上下文这种只读数据用 ScopedValue 或显式参数传递确实要为单个线程保留可变状态再用 ThreadLocal跨任务编排统一交给 StructuredTaskScope。组合起来之后虚拟线程带来的那点内存放大和上下文混乱基本就解掉了。2. 字符串模板的三次预览别急着拿新写法抄作业2.1 拼接痛点为什么值得一个 JEP字符串拼接大概是每个 Java 开发者心里都骂过的事。拼接在编译后可能被优化成StringBuilder但一旦涉及转义、国际化、SQL 语句拼装代码可读性立刻崩掉。字符串模板想解决的不只是“少敲几个加号”而是把“模板字符串”和“怎么处理模板里的内容”彻底分开模板负责表达结构处理器负责定义每个表达式怎么渲染。Java 21 开始以预览特性引入JEP 459Java 22 和 Java 23 各重新预览了一次。基础用法是STR处理器String name 某开发者; int score 87; String report STR.\{name} 这次得了 \{score} 分;对比拼接和String.format模板写法的优势很直接变量就嵌在文本原地顺序和上下文一目了然表达式可以是一段计算比如STR.下次需要 \{score 5} 分。2.2 STR、FMT、RAW 三个处理器的分工模板表达式背后是TemplateProcessor接口STR只是它最常用的实现。我建议把另外两个也记进脑子里FMT支持java.util.Formatter风格的格式说明符适合对齐、补零、控制小数位这些场景比手写String.format和模板混合舒服得多。RAW不直接生成字符串而是返回StringTemplate对象。你拿到的是一组文本片段和一组值等于把“模板”本身变成了可处理的中间产物。StringTemplate st RAW.用户\{name}积分\{points}; System.out.println(st.fragments()); // [用户, 积分, ] System.out.println(st.values()); // [某开发者, 12345]能用RAW做很多自定义处理器的活比如把模板渲染成 HTML 自动转义版本或者按国际化格式重新排列。说白了 STR 只是把所有值toString()拼接起来真正的能力在自定义TemplateProcessor里。2.3 三次预览传递的信号我在本地分别用 JDK 21、22、23 编译过同一段模板代码得到的结论是不要认为第一版预览里的语法会原样转正。从第一次预览到现在官方一直在调整模板体的语法边界、表达式写法、以及和文本块的交互规则属于典型的“方向已经有了细节还在磨”的状态。所以给现阶段想尝鲜的人三条建议第一写 Demo 之前先看一眼当前 JDK 版本对应的 JEP 号确认语法没变第二预览特性必须加--enable-preview --release 22之类参数才能编译库作者尤其要注意你的依赖方未必开了预览开关第三生产代码暂时别大面积铺开等正式版语法定下来再迁移反正字符串拼接不管多丑都不会跑错模板写错了可是会编译失败的。3. 2024 年标准库里的性能武器FFM API 和 Vector API3.1 FFM API 转正JNI 终于可以体面退休JDK 22 里Foreign Function Memory APIFFM APIJEP 454转正这是 Java 平台层面近十年对“跨语言互操作”最大的一次洗牌。以前调用 C 函数只能写 JNI生成头文件、写 C 代码、编译成.so、再在 Java 里加载声明整套流程繁琐不说任何一个JNIEnv操作写错都会把进程直接打崩。FFM API 把这事压缩成了标准库调用。下面是一个调用 C 数学库log函数的完整例子import java.lang.foreign.*; import static java.lang.foreign.ValueLayout.*; try (Arena arena Arena.ofConfined()) { SymbolLookup lib SymbolLookup.libraryLookup(libm.so, arena); MethodHandle log Linker.nativeLinker().downcallHandle( lib.find(log).orElseThrow(), FunctionDescriptor.of(JAVA_DOUBLE, JAVA_DOUBLE) ); double result (double) log.invokeExact(2.718281828); System.out.println(result); }没有 C 文件没有头文件没有 JNI 胶水层。Linker负责传统 ABI 的调用约定Arena管理本地内存的生命周期MemorySegment表示一段可安全访问的内存区域。跟ByteBuffer那种手工指定释放时机的方式比Arena 用 try-with-resources 就能把一整块内存连带释放干净基本告别内存泄漏。对于操作系统的 C 结构体还可以用MemoryLayout描述布局然后通过MemorySegment直接读写字段不需要写一堆繁琐的偏移量计算。我在模拟项目里把一段调用 native 库的业务从 JNI 迁移到 FFM最直观的感受是可以删掉一个完整的构建步骤而且invokeExact的性能基本是零开销调用。唯一的门槛是native 库本身的平台兼容问题依然存在FFM 不是跨平台魔法它只是把调用过程变简单了。3.2 Vector API还在孵化器里的显式向量化如果说 FFM 解决了“和外面互通”那 Vector API 解决的是“榨干机器本身”。它从 Java 16 起就作为孵化器模块存在一直到现在还在迭代JDK 23 的 JEP 469 是第七轮孵化。var species FloatVector.SPECIES_256; float[] a new float[1024]; float[] b new float[1024]; float[] c new float[1024]; for (int i 0; i a.length; i species.length()) { FloatVector va FloatVector.fromArray(species, a, i); FloatVector vb FloatVector.fromArray(species, b, i); // 一次处理 8 个 float等价于手动展开循环 va.mul(vb).add(va).intoArray(c, i); }这段代码比普通循环快的原因是它显式要求 CPU 走 SIMD 指令一次处理一组数据而不是让 JIT 自己猜。听起来很美但实际使用要面对几个现实问题其一--add-modules jdk.incubator.vector这种孵化器开关没法在普通生产环境随便加其二循环尾部的剩余元素要单独处理否则越界其三性能收益和 CPU 架构强相关在老的 x86 机器上可能还不如 JIT 自动向量化。所以我的态度是Vector API 适合确定性要求极高、性能瓶颈明确的热点代码普通业务代码还是让 JIT 自己做向量化更省心。这个 API 什么时候转正比它本身更好用这件事更重要。3.3 什么时候真的需要用这两把武器很多朋友问 FFM 是不是应该把所有 JNI 都换掉。我的判断标准很简单如果你的 Java 层调用 native 库频率很低性能不是瓶颈那纯 Java 的选项够用就别折腾如果调用很频繁、每次传大量数据FFM 的零拷贝和确定性内存管理就是实打实的收益。至于 Vector API先做基准测试用数据说话别为了“用了新 API”而用。4. Class-File API字节码工具链的新地基4.1 标准库为什么非要做字节码解析字节码工具链一直是 Java 生态里最微妙的地基。IDE 的编译错误提示、代码覆盖工具、Mock 框架、各种注解处理器背后都要解析或生成.class文件。这些工具以前要么直接用 JDK 内部未公开接口要么依赖第三方字节码库。问题在于第三方库对“新 class 版本”的跟进永远是滞后的。JDK 每半年发一个新版本class 文件版本号跟着变第三方库要等下一个版本才能支持这中间就形成一段“新版 JDK 编译出来的类老工具解析不了”的尴尬期。Java 22 以预览形式引入的Class-File APIJEP 457本质上是要把字节码解析和生成能力收编进标准库让工具链不再被外部库的发布节奏卡脖子。4.2 用 Class-File API 读类和建类这个 API 的设计思路和过去直接操作“字节数组 偏移量”完全不同它把类文件描述成一组不可变模型解析和生成共用一套接口。读一个类的所有方法名import java.lang.classfile.*; import java.lang.constant.*; ClassFile cf ClassFile.of(); ClassModel cm cf.parse(bytes); for (MethodModel m : cm.methods()) { System.out.println(m.methodName().stringValue()); }生成一个新类byte[] newClass ClassFile.of().build( ClassDesc.of(demo.Sample), cb - cb .withFlags(ClassFile.ACC_PUBLIC) .withMethod(hello, MethodTypeDesc.of(ConstantDescs.CD_void), ClassFile.ACC_PUBLIC, mb - mb.withCode(code - code.return_())) );生成出来的字节码可以直接交给ClassLoader加载。相比手写字节码这套 API 带着类型检查、元素顺序校验、属性过滤很多低级错误在生成阶段就被拦住了。我试过用它做一个“给类加一个日志方法”的小工具写起来比 ASM 的ClassVisitor体系直观不少因为大部分结构细节被隐藏了。4.3 预览阶段的使用注意截至 2024 年Class-File API 还只是预览特性API 形状每一版都可能变我上面贴的这段代码就大概率和几个月后的版本对不上。它真正的意义在于字节码工具链未来会有一个“官方默认选项”第三方库会逐渐迁移到这上面来。对普通应用开发者的影响是间接的——你日常用的框架可能慢慢换掉内部依赖但对写编译器插件、字节码增强工具的人来说现在就是开始熟悉 API 形状的最好时机。正式化之前谨慎用在生产环境。5. 容易被忽略的 2024 细节未命名模式、KEM 接口和一批工具箱更新5.1 未命名变量与未命名模式省的是命名成本Java 22 把Unnamed Variables PatternsJEP 456正式转正语法是在变量名位置写_。它解决的问题很朴素有些位置你根本不关心变量叫什么但不得不给它起个名字。最典型的例子是加锁try (var _ someLock.lock()) { // 业务代码 }模式匹配里也很常见if (point instanceof Point(_, int y)) { System.out.println(只需要用到 y y); }以前这种地方要么写i、o、tmp这种没有信息量的名字要么借用instanceof之后再((Point) p).getY()。_明确告诉读者“这里我们不关心”编译期也不会再警告未使用变量。事很小但代码的长期可读性就是靠这种细节堆起来的。5.2 Key Encapsulation Mechanism给后量子加密留的接口Java 23 新增了Key Encapsulation Mechanism APIJEP 452。名字听着吓人其实概念很直观发送方用一个公钥生成一个对称密钥和它的密文接收方用私钥从密文里解出同一个密钥。这正是 TLS 握手和混合加密场景需要的原语。KeyEncapsulation kem KeyEncapsulation.getInstance(RSA_KEM); KEM.Encapsulated e kem.encapsulate(publicKey); SecretKey secretKey e.secretKey(); byte[] encapsulated e.encapsulation(); // 接收方在另一端 KeyEncapsulation kem2 KeyEncapsulation.getInstance(RSA_KEM); SecretKey sameKey kem2.decapsulate(encapsulated, privateKey);2024 年这个 API 转正更大的意义是给后量子算法留好位置。你不需要立刻在自己的系统里换算法但知道 Java 已经有一等 API 支撑这件事评估未来加密升级的时候就多一个确定项。业务侧想立刻用的人现阶段要注意 JDK 自带的可用算法实例有限具体算法名要去官方安全标准名文档里确认。5.3 这年的废弃清单比新特性更能提醒你升级每年除了新能力我也会特意整理一遍“什么不能用了”因为这些往往比新特性更容易埋雷变化出处/JDK现状影响未命名变量与模式JEP 456 / JDK 22正式老代码不受影响新代码可优化Stream GatherersJEP 461, 473 / JDK 22-23预览流式处理的自定义中间操作Class-File APIJEP 457 / JDK 22预览字节码工具链的未来地基Vector APIJEP 460, 469 / JDK 22-23孵化器新硬件向量化能力Scoped ValuesJEP 464, 481 / JDK 22-23预览并发上下文的下一代方案KEM APIJEP 452 / JDK 23正式后续量子加密算法的接口32 位 x86 移植版废弃JEP 449 / JDK 23废弃老旧的 32 位环境需要迁移sun.misc.Unsafe内存访问方法废弃JEP 471 / JDK 23废弃框架底层要迁移到 VarHandle对大多数应用开发者来说sun.misc.Unsafe的废弃是这些人里面最影响深远的。很多高性能框架和中间件都偷偷依赖它来绕过边界检查官方这次终于给出明确态度逐步废弃迁移到VarHandle和MemorySegment。你作为使用者可能不需要改代码但你的依赖方如果不跟进到未来几个版本就会遇到启动警告甚至强制失败。Stream Gatherers 倒是值得多看一眼虽然还在预览但 API 已经能用了ListListInteger windows IntStream.range(0, 10) .boxed() .gather(Gatherers.windowFixed(3)) .toList();我想强调一下这类“边角”特性的实际影响往往比大特性更早出现在你的构建日志里。每次升级 JDK 之前花半小时扫一遍废弃清单能省去后面好几天排查奇怪告警的时间。回看 2024 年这份清单我的整体感受是 Java 站在一个很有意思的节点虚拟线程把并发模型往前推了一大步FFM 把性能互操作的门槛拉到底字符串模板和 Class-File API 都还在预览里打磨。如果你跟我一样靠“半年一升级、跟趋势”活着最该做的不是急着把这些特性全用上而是先按字段拉一张表哪些转正了、哪些还是预览、哪些已经废弃。把这张表放进每次版本升级的评估流程里比追每一篇新特性介绍都管用。下一章我大概率会写预览特性从孵化到转正的过程里那些被砍掉的 API 和设计取舍因为这些才是最影响你“能不能照着写”的部分。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询