Java JDK 21 核心特性

JDK 21 是 Java 平台的一个重要里程碑,作为最新的长期支持(LTS)版本,它引入了多项革命性的特性,极大地提升了开发效率和应用程序性能。本文档将详细解析 JDK 21 的核心技术更新,帮助开发者快速掌握并应用这些新特性。
1. 版本概览
JDK 21 于 2023 年 9 月正式发布。作为 Oracle 标准支持期限长达 8 年的 LTS 版本,它不仅巩固了先前版本中的预览特性(如虚拟线程、模式匹配),还引入了多项旨在简化编码和提升吞吐量的新功能。
关键总结:JDK 21 标志着 Java 并发编程模型的重大转变,特别是虚拟线程的正式化,使得高并发应用的构建变得前所未有的简单。
2. JEP 444:虚拟线程(Virtual Threads)
虚拟线程是 JDK 21 中最受瞩目的特性,它是轻量级的线程,旨在大幅减少编写、维护和观察高吞吐量并发应用程序的工作量。
2.1 传统线程与虚拟线程的对比
在引入虚拟线程之前,Java 的 java.lang.Thread 是对操作系统(OS)线程的直接包装。这种"平台线程"存在以下局限:
- 资源昂贵:创建和维护 OS 线程需要消耗大量的内存和 CPU 资源。
- 数量受限:受限于 OS 资源,一台机器通常只能运行几千个平台线程。
- 阻塞代价高:当线程执行 I/O 操作阻塞时,底层的 OS 线程也被占用,导致资源浪费。
虚拟线程则不同,它由 JVM 管理,不与特定的 OS 线程绑定(M:N 调度模型)。
| 特性 | 平台线程 (Platform Thread) | 虚拟线程 (Virtual Thread) |
|---|---|---|
| 映射关系 | 1:1 映射到 OS 线程 | M:N 映射到 OS 线程 |
| 创建成本 | 高(毫秒级,MB 级栈内存) | 极低(纳秒级,字节级栈内存) |
| 数量上限 | 几千个 | 数百万个 |
| 调度者 | 操作系统内核 | Java 虚拟机 (JVM) |
| 适用场景 | 计算密集型任务 | I/O 密集型任务 |
2.2 创建虚拟线程
JDK 21 提供了多种创建虚拟线程的方式,API 设计非常直观。
使用静态构建器
java
// 直接启动一个虚拟线程
Thread.ofVirtual().start(() -> {
System.out.println("Hello from Virtual Thread: " + Thread.currentThread());
});
// 创建但不启动
Thread vThread = Thread.ofVirtual()
.name("my-virtual-thread")
.unstarted(() -> {
System.out.println("Running task...");
});
vThread.start();
使用 ExecutorService
为了兼容现有的并发代码,Executors 工具类新增了方法来创建基于虚拟线程的线程池。注意,虚拟线程不需要"池化",因为它们的创建成本极低,这里所谓的"线程池"实际上是为每个任务创建一个新的虚拟线程。
java
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
public class VirtualThreadExample {
public static void main(String[] args) {
// try-with-resources 结构,自动关闭 ExecutorService
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10000; i++) {
int taskId = i;
executor.submit(() -> {
// 模拟耗时 I/O 操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Task " + taskId + " completed.");
});
}
} // executor 在此处自动关闭并等待所有任务完成
}
}
2.3 最佳实践与注意事项
- 不要池化虚拟线程:虚拟线程不仅廉价而且是短暂的,永远不要将它们放入传统的线程池中复用。
- **避免长时间的 Pinning(载体线程绑定)**:当在
synchronized块或方法中执行阻塞操作,或者调用本地方法(JNI)时,虚拟线程会被"钉"在载体线程上,导致无法释放底层的 OS 线程。 建议 :在虚拟线程中,尽量使用ReentrantLock替代synchronized以避免 Pinning 问题,除非是在进行纯内存操作。 - ThreadLocal 的使用 :虽然虚拟线程支持
ThreadLocal,但由于可能存在数百万个线程,如果每个线程都分配大的对象到ThreadLocal,可能会导致堆内存溢出。建议使用Scoped Values(预览特性)作为替代。
3. JEP 431:序列集合(Sequenced Collections)
在 JDK 21 之前,Java 集合框架缺乏一种统一的方式来表示"具有确定出现顺序"的集合。例如,List 和 Deque 定义了顺序,但 SortedSet 也定义了顺序,然而它们没有共同的父接口来描述这种"有序性"。这也导致了访问第一个或最后一个元素的操作在不同集合中不一致。
3.1 接口层级变化
JDK 21 引入了三个新接口来填补这一空白:
SequencedCollection<E>SequencedSet<E>SequencedMap<K,V>
新的继承关系如下:
List extends SequencedCollectionDeque extends SequencedCollectionSortedSet extends SequencedSetLinkedHashSet implements SequencedSetSortedMap extends SequencedMapLinkedHashMap implements SequencedMap
3.2 统一的操作方法
SequencedCollection 接口提供了一组标准方法来处理首尾元素:
| 方法 | 描述 |
|---|---|
void addFirst(E e) |
在集合开头添加元素(如果支持) |
void addLast(E e) |
在集合末尾添加元素(如果支持) |
E getFirst() |
获取第一个元素 |
E getLast() |
获取最后一个元素 |
E removeFirst() |
移除并返回第一个元素 |
E removeLast() |
移除并返回最后一个元素 |
SequencedCollection reversed() |
返回集合的逆序视图 |
3.3 代码示例
以下展示了如何在常用的 ArrayList 和 LinkedHashSet 中使用新 API:
java
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.SequencedCollection;
import java.util.SequencedSet;
public class SequencedCollectionsDemo {
public static void main(String[] args) {
// List 示例
SequencedCollection<String> list = new ArrayList<>();
list.addLast("Apple");
list.addFirst("Banana"); // Banana, Apple
list.addLast("Orange"); // Banana, Apple, Orange
System.out.println("First: " + list.getFirst()); // Banana
System.out.println("Last: " + list.getLast()); // Orange
// Set 示例 (LinkedHashSet 保持插入顺序)
SequencedSet<String> set = new LinkedHashSet<>();
set.add("One");
set.add("Two");
set.addFirst("Zero"); // Zero, One, Two
System.out.println("Original Set: " + set);
System.out.println("Reversed Set: " + set.reversed()); // Two, One, Zero
}
}
注意 :对于
SortedSet(如TreeSet),addFirst和addLast方法会抛出UnsupportedOperationException,因为排序集合的顺序是由元素比较规则决定的,不能手动指定位置。
4. JEP 441:Switch 模式匹配(Pattern Matching for Switch)
Switch 表达式在 JDK 21 中得到了极大的增强,现在支持类型匹配,这使得编写多态代码变得更加简洁和安全,消除了大量的 if-else 和强制类型转换。
4.1 基本类型匹配
传统的 Switch 只能支持整数、枚举和字符串。现在,case 标签可以包含类型模式。
java
static String formatterPatternSwitch(Object obj) {
return switch (obj) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> obj.toString();
};
}
4.2 Guard 子句(when 关键字)
我们可以在模式匹配中添加布尔表达式来细化匹配逻辑,使用 when 关键字。这避免了在 case 块内部再写 if 语句。
java
static void testTriangle(Shape s) {
switch (s) {
case Triangle t when t.calculateArea() > 100 ->
System.out.println("Large triangle");
case Triangle t ->
System.out.println("Small triangle");
default ->
System.out.println("Unknown shape");
}
}
4.3 Null 处理
以前,Switch 语句遇到 null 会直接抛出 NullPointerException。现在,可以显式处理 null 情况:
java
static void testNull(String s) {
switch (s) {
case null -> System.out.println("Oops, it is null!");
case "Yes", "Y" -> System.out.println("It's Yes");
case "No", "N" -> System.out.println("It's No");
default -> System.out.println("Something else");
}
}
5. JEP 440:记录模式(Record Patterns)
记录模式允许在模式匹配中直接解构 Record 类的值。这一特性与 Switch 模式匹配结合使用时非常强大,可以极大地简化数据导航和处理逻辑。
5.1 什么是 Record 解构?
假设我们有以下 Record 定义:
java
record Point(int x, int y) {}
record ColoredPoint(Point p, String color) {}
在 JDK 21 之前,我们需要这样判断并取值:
java
Object obj = new Point(10, 20);
if (obj instanceof Point p) {
int x = p.x();
int y = p.y();
// 使用 x 和 y
}
在 JDK 21 中,我们可以直接解构:
java
if (obj instanceof Point(int x, int y)) {
System.out.println("x = " + x + ", y = " + y);
}
5.2 嵌套模式匹配
记录模式真正的威力在于处理嵌套结构。
java
static void printColorOfUpperRightPoint(Object obj) {
// 嵌套解构:直接深入到 ColoredPoint -> Point -> x, y
if (obj instanceof ColoredPoint(Point(int x, int y), String color)) {
System.out.println("Color: " + color + ", Position: " + x + ", " + y);
}
}
还可以结合 var 使用以简化类型声明:
java
if (obj instanceof ColoredPoint(Point(var x, var y), var color)) {
// ...
}
6. JEP 439:分代 ZGC(Generational ZGC)
ZGC(Z Garbage Collector)最初在 JDK 11 作为实验性特性引入,旨在实现低延迟垃圾回收。在 JDK 21 中,ZGC 引入了分代假说(Generational Hypothesis),即"大多数对象都是朝生夕死的"。
6.1 性能提升
分代 ZGC 将堆内存划分为年轻代 (Young Generation)和老年代(Old Generation)。
- 更高效的回收:专注于频繁回收年轻代,因为那里充满了垃圾。
- 更低的 CPU 开销:减少了扫描长寿命对象(老年代)的频率。
6.2 启用方式
在 JDK 21 中,分代 ZGC 尚未默认启用,需要通过命令行参数开启:
bash
java -XX:+UseZGC -XX:+ZGenerational ...
性能数据:根据 Oracle 的测试,分代 ZGC 相比非分代 ZGC,吞吐量提升了 10% 以上,且维持了亚毫秒级的暂停时间。对于大内存应用(数百 GB 甚至 TB 级),这是极佳的选择。
7. 其他值得关注的特性
7.1 Key Encapsulation Mechanism API (JEP 452)
为密钥封装机制(KEM)引入了新的 API,这是一种利用公钥加密来保护对称密钥的高级加密技术,对后量子加密算法的支持至关重要。
7.2 Prepare to Disallow the Dynamic Loading of Agents (JEP 451)
当代理试图动态加载到正在运行的 JVM 时发出警告。这是为了最终默认禁止该操作做准备,以提高默认情况下的安全性。
7.3 String Templates (JEP 430 - 预览特性)
虽然是预览特性,但它非常重要。它允许在字符串中嵌入表达式,比传统的 + 拼接或 String.format 更安全、更易读。
java
String name = "Joan";
String info = STR."My name is \{name}";
8. 从旧版本迁移的建议
如果您计划从 Java 8、11 或 17 迁移到 JDK 21,请参考以下步骤:
- 依赖升级:确保您的构建工具(Maven/Gradle)和第三方库(如 Spring Boot, Lombok)支持 JDK 21。Spring Boot 3.2+ 对 JDK 21 有很好的支持。
- 代码扫描 :使用
jdeprscan工具检查代码中是否使用了已废弃的 API。 - Lombok 兼容性:如果您使用 Lombok,请务必升级到 1.18.30 或更高版本,以修复与新编译器的一些兼容性问题。
- 虚拟线程试点:不要试图一次性将所有线程池替换为虚拟线程。建议先在 I/O 密集型的独立模块中尝试,观察性能指标和资源使用情况。
总结
JDK 21 凭借虚拟线程 和序列集合等特性,不仅解决了 Java 长期以来的痛点,还为云原生应用开发提供了强大的基础设施支持。无论是为了提升并发性能,还是为了享受更现代化的语法糖,升级到 JDK 21 都是一个极具价值的选择。
参考资源