Skip to content

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 18Javadoc代码片段更可靠的文档示例
JDK 21虚拟线程高并发阻塞式IO编程
JDK 21Record模式解构Record组件
JDK 21Switch模式匹配按类型和条件分支
JDK 21Sequenced Collections统一首尾和逆序访问
JDK 21分代ZGC低延迟GC的分代模式
JDK 21KEM API密钥封装机制的统一API

JDK 21中的Preview/Incubator

能力JDK 21状态生产态度
String TemplatesPreview后续演进发生变化,不作为稳定语法依赖
Scoped ValuesPreview评估上下文传递,等待正式化
Structured ConcurrencyPreview适合实验任务生命周期结构化
Foreign Function & Memory API第三次PreviewJDK 22后才正式,21中仍需Preview
Unnamed Patterns and VariablesPreview语法可能变化
Unnamed Classes and Instance MainPreview教学和小程序实验

二、虚拟线程是什么

传统平台线程通常与操作系统线程较紧密绑定;虚拟线程由JDK调度,大量虚拟线程可以复用较少的载体线程。它的目标是让“一请求一线程”的同步代码在高并发IO等待场景下具有更好的伸缩性。

mermaid
flowchart TD
    A["大量业务任务"] --> B["大量虚拟线程"]
    B --> C["JDK调度器"]
    C --> D["少量Carrier平台线程"]
    D --> E["操作系统线程"]

虚拟线程不会让CPU计算变快。它主要减少大量阻塞等待线程的资源成本:数据库、HTTP、文件和队列等待仍然存在,下游容量也不会因为线程便宜而变大。

三、虚拟线程如何使用

3.1 每任务一个虚拟线程

java
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 直接启动

java
Thread thread = Thread.startVirtualThread(() -> {
    System.out.println("running in " + Thread.currentThread());
});
thread.join();

3.3 Builder

java
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限制下游并发:

java
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监视器或特定本地调用中,可能固定在载体线程上,降低伸缩性。

java
synchronized (lock) {
    // JDK 21中在监视器内执行长时间阻塞IO可能pin住Carrier
    remoteClient.call();
}

诊断:

bash
-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”整体视为完全相同。

常见配置形态:

yaml
spring:
  threads:
    virtual:
      enabled: true

开启后仍需验证:

  • Web请求是否实际运行在虚拟线程。
  • @Async、任务调度和自定义Executor是否改变。
  • JDBC驱动、HTTP客户端和Agent是否兼容。
  • ThreadLocal/MDC/Tracing是否正确传播。
  • Hikari连接池是否成为新瓶颈。
  • 压测下CPU、RSS、P99和下游并发。
  • 优雅停机是否等待在途任务。

不要仅看“线程数更多”就判断收益,必须和平台线程基线比较同等吞吐下的内存、CPU和延迟。

九、Record模式

JDK 21可以在类型匹配时直接解构Record组件:

java
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模式:

java
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模式匹配

java
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

java
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为有明确遇到顺序的集合统一首尾和逆序操作。

java
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
java
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:

bash
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,提高跨操作系统一致性。

java
Charset charset = Charset.defaultCharset();

升级风险:旧系统可能依赖Windows本地编码或历史容器默认编码。推荐始终显式指定协议和文件编码:

java
Files.readString(path, StandardCharsets.UTF_8);
text.getBytes(StandardCharsets.UTF_8);

默认UTF-8不能修复已有错误编码数据,也不能替代数据库、HTTP Header、CSV和消息协议中的显式编码约定。

十四、简单Web服务器

JDK 18提供命令行静态文件服务器:

bash
jwebserver -p 8000 -d ./public

适合本地分享静态文件、Demo和测试,不是生产Web容器:缺少完整认证、动态业务、治理和安全加固能力。

十五、Javadoc代码片段

JDK 18引入@snippet改善文档代码示例:

java
/**
 * 创建订单。
 * {@snippet :
 * Order order = service.create(command);
 * System.out.println(order.id());
 * }
 */

可以从外部文件引用并高亮区域,降低文档代码与真实示例失去同步的风险。仍建议在构建中编译或测试示例源文件。

十六、Preview特性怎么使用

编译:

bash
javac --release 21 --enable-preview Demo.java

运行:

bash
java --enable-preview Demo

Maven需要编译和测试运行都开启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

mermaid
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、连接池和限流
虚拟线程也要建固定大小池通常每任务创建一个,资源在下游处限流
虚拟线程完全没有pinningJDK 21需关注监视器和本地调用pinning
Record模式来自JDK 17Record类型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和下游容量都满足目标。