JDK 21 新增特性、使用方法与迁移实战
JDK 21是继17之后的重要LTS。它最受关注的是虚拟线程,但完整价值还包括Record模式、Switch模式匹配、Sequenced Collections、分代ZGC,以及JDK 18~21期间的默认UTF-8、简单Web服务器等工程能力。
必须先区分:JDK 21中既有正式特性,也有Preview和Incubator能力。Preview需要--enable-preview,API和语法可能在后续版本变化、撤回或再次预览,普通生产项目不应把它们当稳定基线。
一、JDK 18到21能力地图
正式能力
| 首次版本 | 能力 | 生产价值 |
|---|---|---|
| JDK 18 | 默认UTF-8 | 跨环境字符集行为更一致 |
| JDK 18 | 简单Web服务器 | 本地静态文件和测试 |
| JDK 18 | Javadoc代码片段 | 更可靠的文档示例 |
| JDK 21 | 虚拟线程 | 高并发阻塞式IO编程 |
| JDK 21 | Record模式 | 解构Record组件 |
| JDK 21 | Switch模式匹配 | 按类型和条件分支 |
| JDK 21 | Sequenced Collections | 统一首尾和逆序访问 |
| JDK 21 | 分代ZGC | 低延迟GC的分代模式 |
| JDK 21 | KEM API | 密钥封装机制的统一API |
JDK 21中的Preview/Incubator
| 能力 | JDK 21状态 | 生产态度 |
|---|---|---|
| String Templates | Preview | 后续演进发生变化,不作为稳定语法依赖 |
| Scoped Values | Preview | 评估上下文传递,等待正式化 |
| Structured Concurrency | Preview | 适合实验任务生命周期结构化 |
| Foreign Function & Memory API | 第三次Preview | JDK 22后才正式,21中仍需Preview |
| Unnamed Patterns and Variables | Preview | 语法可能变化 |
| Unnamed Classes and Instance Main | Preview | 教学和小程序实验 |
二、虚拟线程是什么
传统平台线程通常与操作系统线程较紧密绑定;虚拟线程由JDK调度,大量虚拟线程可以复用较少的载体线程。它的目标是让“一请求一线程”的同步代码在高并发IO等待场景下具有更好的伸缩性。
flowchart TD
A["大量业务任务"] --> B["大量虚拟线程"]
B --> C["JDK调度器"]
C --> D["少量Carrier平台线程"]
D --> E["操作系统线程"]虚拟线程不会让CPU计算变快。它主要减少大量阻塞等待线程的资源成本:数据库、HTTP、文件和队列等待仍然存在,下游容量也不会因为线程便宜而变大。
三、虚拟线程如何使用
3.1 每任务一个虚拟线程
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<User> userFuture = executor.submit(() -> userClient.get(userId));
Future<List<Order>> orderFuture = executor.submit(() -> orderClient.list(userId));
User user = userFuture.get();
List<Order> orders = orderFuture.get();
return new UserView(user, orders);
}虚拟线程推荐“每个并发任务创建一个”,通常不需要像平台线程那样池化。newVirtualThreadPerTaskExecutor为每个提交任务创建虚拟线程。
3.2 直接启动
Thread thread = Thread.startVirtualThread(() -> {
System.out.println("running in " + Thread.currentThread());
});
thread.join();3.3 Builder
ThreadFactory factory = Thread.ofVirtual()
.name("order-worker-", 0)
.factory();
Thread thread = factory.newThread(this::processOrder);
thread.start();四、虚拟线程适合什么
| 适合 | 原因 |
|---|---|
| 大量HTTP/RPC等待 | 阻塞时虚拟线程通常可卸载载体线程 |
| 数据库访问 | 保留同步JDBC写法,线程等待成本降低 |
| 高并发网关后业务编排 | 同步代码比回调链更易读 |
| 每任务独立的阻塞流程 | 生命周期自然映射到线程 |
不适合或收益有限:
| 场景 | 原因 |
|---|---|
| CPU密集计算 | CPU核心数量不变 |
| 长时间占用本地代码或发生pinning | 载体线程不能及时释放 |
| 下游连接池很小 | 并发只会堆积在连接池前 |
| 依赖大量ThreadLocal缓存大对象 | 虚拟线程数量大导致内存压力 |
| 依赖固定线程身份的旧组件 | 线程语义假设可能失效 |
五、虚拟线程不等于无限并发
假设数据库最多安全处理100个并发查询,即使可以创建10万个虚拟线程,也不能让10万个任务同时访问数据库。
使用Semaphore限制下游并发:
private final Semaphore databasePermits = new Semaphore(100);
Order loadOrder(long orderId) throws InterruptedException {
databasePermits.acquire();
try {
return repository.findById(orderId);
} finally {
databasePermits.release();
}
}需要同时设置:
- HTTP客户端连接和请求超时。
- 数据库连接池上限。
- 入口限流。
- 下游并发隔离。
- 总任务时限和取消。
- 队列/积压指标。
虚拟线程解决线程成本,不解决容量、背压、超时和雪崩。
六、Pinning
在JDK 21中,虚拟线程执行某些阻塞操作时如果处在synchronized监视器或特定本地调用中,可能固定在载体线程上,降低伸缩性。
synchronized (lock) {
// JDK 21中在监视器内执行长时间阻塞IO可能pin住Carrier
remoteClient.call();
}诊断:
-Djdk.tracePinnedThreads=full也可以使用JFR的虚拟线程相关事件。修复方向:
- 缩小
synchronized范围。 - 不在监视器内调用慢IO。
- 评估使用
ReentrantLock。 - 升级后重新验证,因为后续JDK对pinning行为持续改进。
回答JDK 21时必须按21的行为说明,不能把后续JDK优化倒推到21。
七、ThreadLocal和可观测性
虚拟线程支持ThreadLocal,但数量可能非常大:
- 不要为每个线程缓存大型对象或昂贵资源。
- 数据库连接不能长期绑定到虚拟线程。
- MDC、Trace、SecurityContext传播要使用框架的兼容实现。
- ThreadLocal必须清理,但频繁创建的虚拟线程降低了线程复用污染问题,不表示内存成本消失。
- 监控应看任务吞吐、失败、下游等待,而不是只看传统线程池active数量。
Scoped Values在JDK 21仍是Preview,可作为不可变上下文传播的实验方向,但不能当作JDK 21稳定API承诺。
八、Spring Boot中使用虚拟线程
较新的Spring Boot 3.x版本提供虚拟线程集成配置,但具体起始小版本、Web容器、调度和执行器行为要查项目使用版本,不能把“Boot 3”整体视为完全相同。
常见配置形态:
spring:
threads:
virtual:
enabled: true开启后仍需验证:
- Web请求是否实际运行在虚拟线程。
@Async、任务调度和自定义Executor是否改变。- JDBC驱动、HTTP客户端和Agent是否兼容。
- ThreadLocal/MDC/Tracing是否正确传播。
- Hikari连接池是否成为新瓶颈。
- 压测下CPU、RSS、P99和下游并发。
- 优雅停机是否等待在途任务。
不要仅看“线程数更多”就判断收益,必须和平台线程基线比较同等吞吐下的内存、CPU和延迟。
九、Record模式
JDK 21可以在类型匹配时直接解构Record组件:
record Point(int x, int y) {}
static int distanceSquared(Object value) {
if (value instanceof Point(int x, int y)) {
return x * x + y * y;
}
return 0;
}嵌套Record模式:
record Address(String city, String street) {}
record User(String name, Address address) {}
static String cityOf(Object value) {
if (value instanceof User(String name, Address(String city, String street))) {
return city;
}
return "UNKNOWN";
}它适合解析不可变数据、协议消息和代数式类型。嵌套过深会降低可读性,复杂校验应拆成命名方法。
十、Switch模式匹配
static String describe(Object value) {
return switch (value) {
case null -> "NULL";
case Integer number when number > 0 -> "正整数:" + number;
case Integer number -> "非正整数:" + number;
case String text when text.isBlank() -> "空白文本";
case String text -> "文本:" + text;
default -> "其他类型";
};
}能力:
- 按类型分支并绑定变量。
- 使用
when守卫追加条件。 - 显式处理
null。 - 编译器检查分支支配关系和穷尽性。
顺序很重要:宽泛类型不能放在更具体类型前面,否则后者不可到达。守卫条件应保持无副作用和易读。
配合sealed和record
sealed interface PayResult permits Success, Rejected {}
record Success(String tradeNo) implements PayResult {}
record Rejected(String reason) implements PayResult {}
static String message(PayResult result) {
return switch (result) {
case Success(String tradeNo) -> "成功:" + tradeNo;
case Rejected(String reason) -> "拒绝:" + reason;
};
}编译器知道sealed层次的直接子类型集合,可以检查是否覆盖全部情况。这是JDK 17的Record和sealed在JDK 21中进一步组合的价值。
十一、Sequenced Collections
JDK 21为有明确遇到顺序的集合统一首尾和逆序操作。
SequencedCollection<String> queue = new ArrayList<>();
queue.addFirst("first");
queue.addLast("last");
String first = queue.getFirst();
String last = queue.getLast();
SequencedCollection<String> reversed = queue.reversed();层次:
| 接口 | 说明 |
|---|---|
SequencedCollection | 有遇到顺序,支持首尾和逆序视图 |
SequencedSet | 有顺序且元素唯一 |
SequencedMap | 有顺序的键值映射,支持首尾Entry |
SequencedMap<String, Integer> scores = new LinkedHashMap<>();
scores.putFirst("URGENT", 100);
scores.putLast("NORMAL", 50);
Map.Entry<String, Integer> first = scores.firstEntry();reversed()通常是反向视图,不应假设为独立复制;对原集合或视图的允许修改可能相互反映,具体看实现和接口契约。
十二、分代ZGC
JDK 21提供分代ZGC:
java -XX:+UseZGC -XX:+ZGenerational -jar app.jar分代假设大量对象朝生夕死,将年轻对象与长寿对象区别处理,目标是在保持低停顿的同时提高分配回收效率。
生产评估指标:
- P95/P99/P999停顿和业务延迟。
- 吞吐和CPU成本。
- Heap占用和容器RSS。
- 分配速率、晋升和并发GC周期。
- 极端流量与内存压力下的余量。
ZGC不是“零停顿”,也不保证适合所有小堆和吞吐优先场景。应与现有G1在真实负载下对比,而不是只改参数上线。
十三、默认UTF-8
JDK 18起,标准Java API的默认字符集在常见场景统一为UTF-8,提高跨操作系统一致性。
Charset charset = Charset.defaultCharset();升级风险:旧系统可能依赖Windows本地编码或历史容器默认编码。推荐始终显式指定协议和文件编码:
Files.readString(path, StandardCharsets.UTF_8);
text.getBytes(StandardCharsets.UTF_8);默认UTF-8不能修复已有错误编码数据,也不能替代数据库、HTTP Header、CSV和消息协议中的显式编码约定。
十四、简单Web服务器
JDK 18提供命令行静态文件服务器:
jwebserver -p 8000 -d ./public适合本地分享静态文件、Demo和测试,不是生产Web容器:缺少完整认证、动态业务、治理和安全加固能力。
十五、Javadoc代码片段
JDK 18引入@snippet改善文档代码示例:
/**
* 创建订单。
* {@snippet :
* Order order = service.create(command);
* System.out.println(order.id());
* }
*/可以从外部文件引用并高亮区域,降低文档代码与真实示例失去同步的风险。仍建议在构建中编译或测试示例源文件。
十六、Preview特性怎么使用
编译:
javac --release 21 --enable-preview Demo.java运行:
java --enable-preview DemoMaven需要编译和测试运行都开启Preview;否则可能编译成功但测试JVM无法加载。Preview生成的Class只能在相同主版本并开启Preview的运行时执行。
生产原则:
- 稳定业务默认不用Preview。
- 实验项目明确隔离模块和升级成本。
- 不对外发布依赖Preview的长期公共SDK。
- 每次升级重新编译、测试API和语法。
- 不把JDK 21 Preview特性写进“21正式新增”列表。
十七、Structured Concurrency和Scoped Values
二者在JDK 21仍是Preview,仅用于理解方向。
Structured Concurrency把一组子任务视为一个生命周期单元,目标是统一成功、失败、取消和可观测性;Scoped Values用于在线程调用树中传递不可变上下文,避免滥用可变ThreadLocal。
概念示例需要Preview API,实际签名可能随版本变化,因此项目文档不提供可直接长期复制的固定生产代码。稳定项目应继续使用成熟Executor、CompletableFuture或框架抽象,并跟踪正式版本。
十八、Foreign Function & Memory API
FFM用于更安全高效地调用本地函数和操作堆外内存,目标是减少JNI样板。在JDK 21仍是Preview,到后续版本才正式;21项目若使用必须启用Preview并接受API升级成本。
适用方向:数据库/压缩/AI本地库、内存映射和高性能计算。不应为普通业务CRUD引入额外本地内存生命周期和跨平台复杂度。
十九、JDK 17升级21 Runbook
flowchart TD
A["确认框架、驱动、Agent支持21"] --> B["升级构建工具和测试插件"]
B --> C["先用平台线程验证运行兼容"]
C --> D["建立CPU、RSS、GC、P99基线"]
D --> E["按场景灰度虚拟线程"]
E --> F["检查pinning、ThreadLocal和下游容量"]
F --> G["评估分代ZGC和新语言API"]
G --> H["回滚演练与生产灰度"]重点检查:
- Spring Boot、Tomcat/Jetty/Netty的准确版本。
- JDBC驱动、连接池、HTTP客户端和MQ客户端。
- Byte Buddy、ASM、Mockito、Lombok、MapStruct。
- APM、Profiler、安全和动态Attach Agent。
- Maven/Gradle、Surefire、JaCoCo、jlink/jpackage。
- 虚拟线程中的ThreadLocal、MDC、Trace和事务上下文。
synchronized内阻塞导致的pinning。- 数据库连接池和下游限流。
- UTF-8默认行为和历史文件编码。
- GC、RSS、CPU throttling、P99和线程Dump工具链。
建议先“只换JDK不启虚拟线程”,证明运行兼容后再单独打开虚拟线程;否则出问题时难区分版本兼容还是并发模型变化。
二十、常见误区
| 误区 | 正确理解 |
|---|---|
| 虚拟线程让代码执行更快 | 主要提高阻塞IO并发伸缩,不加速CPU计算 |
| 虚拟线程可以无限访问数据库 | 下游容量仍需Semaphore、连接池和限流 |
| 虚拟线程也要建固定大小池 | 通常每任务创建一个,资源在下游处限流 |
| 虚拟线程完全没有pinning | JDK 21需关注监视器和本地调用pinning |
| Record模式来自JDK 17 | Record类型16正式,Record模式21正式 |
| Switch模式在17已经正式 | JDK 21才正式 |
| JDK 21所有JEP都是正式API | 多项仍是Preview/Incubator |
| 分代ZGC适合所有服务 | 要与G1按真实堆和负载对比 |
| 默认UTF-8后不用写charset | 协议和文件仍应显式约定 |
二十一、面试回答
JDK 21主要新增了什么
JDK 21正式引入虚拟线程,让高并发阻塞式IO代码以每任务一线程方式获得更好的伸缩性;正式提供Record模式和Switch模式匹配,支持类型分支、守卫和数据解构;增加Sequenced Collections统一集合首尾与逆序访问;提供分代ZGC。若从17升级,还会获得JDK 18默认UTF-8、简单Web服务器和Javadoc snippet等能力。Structured Concurrency、Scoped Values、FFM和String Templates在21中仍是Preview或Incubator,不能当成稳定正式能力。
虚拟线程和线程池有什么区别
平台线程昂贵,传统线程池通过复用有限线程控制线程资源;虚拟线程很轻,通常每个任务创建一个,不需要池化。并发限制应放在数据库、HTTP下游等稀缺资源前。虚拟线程不提高CPU核心数,仍要设置超时、取消、限流和连接池。
虚拟线程有哪些坑
JDK 21中在synchronized监视器内做长阻塞IO可能pin住载体线程;大量ThreadLocal大对象会放大内存;旧Agent和框架可能不兼容;线程便宜会让请求更快涌向数据库和下游。因此要监控pinning、RSS、下游等待、连接池和业务P99,并通过容量控制限制并发。
JDK 17升级21如何降低风险
先升级构建链、框架、驱动和Agent,只更换运行JDK并保留原并发模型,验证功能和性能;然后把虚拟线程作为独立变更灰度,检查ThreadLocal、pinning、连接池和下游容量;最后再评估分代ZGC和新语言API,并保留回滚开关。
二十二、关联学习
| 专题 | 内容 |
|---|---|
| LTS版本演进总览 | 四个LTS版本对比和选型 |
| JDK 17新增特性 | 17到21升级起点 |
| Java 17现代基线 | 模块、容器和强封装基础 |
| 线程池全过程 | 平台线程池和容量治理 |
| JVM性能调优 | GC、内存和JFR |
| Spring Boot生产排查 | 线上线程、内存和连接池诊断 |
本章小结
JDK 21最重要的变化是让同步阻塞式代码重新获得高并发伸缩性,并让类型分支和数据解构进入正式语言能力。但虚拟线程不是无限资源,Preview也不是稳定承诺。生产升级应分离“运行时兼容”和“启用新并发模型”,用真实负载证明CPU、内存、GC、P99和下游容量都满足目标。
