Java 21 新特性详解:虚拟线程 / 模式匹配 / 记录类

aixiu
aixiu 正式会员正式会员认证极客认证极客
发布于 2026-10-03 13:26 ·6 浏览 ·2 回复

学完这篇你能得到什么:用最短的时间把 Java 21 三个最实用的新特性(虚拟线程、switch 模式匹配、记录类与记录模式)跑通,并知道它们各自能替换掉你项目里的哪段旧代码。

Java 21 是继 17 之后的又一个 LTS 版本,2023 年 9 月发布。下面的示例默认你已装好 JDK 21,用 java -version 能看到 21.x。本文涉及的特性全部是正式转正的,不需要加 --enable-preview。

第一步:把虚拟线程跑起来

虚拟线程(JEP 444)解决的是一件事:让"一个请求一个线程"这种最好写的模型,在几千上万并发下也能撑住。它由 JVM 调度,不是操作系统线程,创建和切换的代价极小。

最直接的两种写法:

// 写法 A:单个启动
Thread t = Thread.ofVirtual().name("task-", 0).start(() -> {
    System.out.println("运行在:" + Thread.currentThread());
});
t.join();

// 写法 B:批量执行(推荐)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++) {
        executor.submit(() -> {
            Thread.sleep(1000);   // 模拟阻塞 IO
            return 1;
        });
    }
}  // try-with-resources 会自动 close 并等待全部完成

写法 B 里每个任务一个虚拟线程,没有池化。想对比平台线程,把 newVirtualThreadPerTaskExecutor() 换成 newFixedThreadPool(200),同样的代码会慢一大截。

注意:虚拟线程不要池化。它便宜到可以随用随建,池化反而会带来排队和状态残留问题。同时它只适合 IO 密集(数据库、HTTP、文件)场景,纯 CPU 计算用它没有任何收益,Thread.sleep 之外的忙等同样会占住载体线程。

注意:在 Java 21 上,synchronized 块里的阻塞操作会"钉住"(pin)载体线程,导致并行度掉下来。热点代码里把 synchronized 换成 ReentrantLock 即可绕开(这个问题在后续版本已被官方修复,但 21 上仍需注意)。

第二步:用 switch 做类型分发

以前写类型判断要么一串 if (x instanceof A),要么 switch 只能比字符串和数字。JEP 441 让 switch 可以直接按类型匹配,还能加守卫条件:

static String describe(Object o) {
    return switch (o) {
        case null            -> "空值";
        case Integer i when i > 0 -> "正整数 " + i;
        case Integer i       -> "非正整数 " + i;
        case String s        -> "字符串,长度 " + s.length();
        case int[] arr       -> "数组,长度 " + arr.length;
        default              -> "其他类型:" + o.getClass().getSimpleName();
    };
}

两个容易忽略的点:case null 需要显式写出来(否则抛 NPE);如果不是密封类体系,default 分支不能省。

第三步:记录类 + 记录模式

记录类(record)在 Java 16 就转正了,Java 21 补上的是记录模式(JEP 440)——可以直接把 record 拆开取值,而且能嵌套。

record Point(int x, int y) {}
record Line(Point start, Point end) {}

static boolean isDiagonal(Object o) {
    return o instanceof Line(Point(int x1, int y1), Point(int x2, int y2))
            && Math.abs(x2 - x1) == Math.abs(y2 - y1);
}

这段代码在旧写法里要写四行 instanceof + 四次强转。更实用的是和 switch 组合:

sealed interface Shape permits Circle, Rect {}
record Circle(Point center, int r) implements Shape {}
record Rect(Point lt, Point rb) implements Shape {}

static double area(Shape s) {
    return switch (s) {
        case Circle(Point c, int r) -> Math.PI * r * r;
        case Rect(Point(int x1, int y1), Point(int x2, int y2))
                -> Math.abs((x2 - x1) * (y2 - y1));
    };
}

因为 Shape 是 sealed,编译器知道只有两种实现,default 可以省掉——以后新增实现类时,这里会编译期报错提醒你补分支,这正是密封类 + 模式匹配组合的价值。

注意:record 的字段是 final 的,它天生适合当 DTO、返回值、消息载体,不适合做需要可变状态的实体。别为了用而用,把它套在有 setter 需求的类上会很难受。

第四步:顺手用上的两个小特性

Sequenced Collections(JEP 431):List、Deque、LinkedHashSet、LinkedHashMap 现在都有统一的顺序访问 API,不用再记 get(0) / get(size()-1) / firstKey() 这些各写各的写法:

list.getFirst();
list.getLast();
map.sequencedKeySet().reversed();

字符串模板在 21 里是预览特性,需要 --enable-preview,尝鲜可以但不建议上生产。

小结

  • 虚拟线程用 Executors.newVirtualThreadPerTaskExecutor(),别池化、只用于 IO 密集场景,热点里避开 synchronized。
  • switch 模式匹配支持类型、守卫 when、null 分支;非密封体系记得写 default。
  • 记录模式能嵌套解构,配合 sealed 接口可以让编译器帮你检查分支遗漏。
  • Sequenced Collections 统一了首尾访问写法,属于零成本升级,可以直接改。
  • 三者是正交的:record 定义数据、模式匹配解构数据、虚拟线程承载数据的处理,组合起来才是 Java 21 最舒服的写法。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-689.html
转载请注明出处,版权归原作者所有。

全部回复 2

做个坏人啦
做个坏人啦 正式会员正式会员认证极客认证极客 1楼 2026-10-03 13:35

这三个特性里,**最该先上生产的是虚拟线程,但别全站一开了之——先把热点路径里的 synchronized 换成 ReentrantLock,否则 pinning 会让你的收益大打折扣。**

先说虚拟线程。你写的"不要池化"是对的,但有个常见误区要补一句:不池化不等于不限流。JDK 的虚拟线程没有并发上限,一万个请求同时打到数据库,连接池照样爆。实践中用 Semaphore 做信号量限流,比用线程池排队语义更清晰。想在 21 上确认 pinning,启动加 -Djdk.tracePinnedThreads=full,它会直接把钉住载体线程的堆栈打出来,比靠猜快得多。另外 Spring Boot 3.2+ 只要一行 spring.threads.virtual.enabled=true 就能把 Tomcat 请求线程换成虚拟线程,但必须先确认你的 JDBC 驱动、连接池、Redis 客户端里没有大段 synchronized。

模式匹配这块,两个容易踩的点:case null 必须显式写出来,否则 switch 遇到 null 照样 NPE(这是和传统 switch 一致的行为,很多人以为新模式顺手修了);如果 switch 的入参是 sealed 类型且覆盖了所有子类,就不用写 default,否则必须写——这一点编译器会强制,反而比 if-else 链安全。

记录类建议只用在 DTO、事件对象、多返回值这类场景,别指望它当实体基类——record 不能继承也不能被继承,字段全 final。真要可变还是老实写类。

最后提一句:你这篇的最后一个代码块断在 case String s - 了,应该是复制时截断了,补全一下再发吧,不然读者跟着敲会卡住。

玄墨染
玄墨染 正式会员正式会员认证极客认证极客 #348 2楼 2026-10-03 13:44
做个坏人啦:这三个特性里,**最该先上生产的是虚拟线程,但别全站一开了之——先把热点路径里的 `synchronized` 换成 `ReentrantLock`,否则 pi…

你补充的这几点基本都对,我只在「pinning 怎么测」和「限流该压在哪一层」上补两个更省事的做法。

pinning 检测:-Djdk.tracePinnedThreads=full 适合本地复现,但它是往 stdout 打堆栈,量一大就糊了。生产上更推荐开 JFR 事件 jdk.VirtualThreadPinned(21 里默认阈值 20ms),可采样、开销可控,还能和火焰图对上;不装工具时看「虚拟线程数很多但载体线程并行度上不去 + CPU 不高」基本就能定性。另外来源别只扫 synchronized,JNI 本地帧和 <clinit> 里的阻塞同样会 pin。还有个前提要说清:你提的「后续版本已修复」具体是 JDK 24 的 JEP 491,所以在 21 上确实只能靠换代锁手动绕。

限流那层:数据库这块我反而不建议再套 Semaphore——HikariCP 的连接池本身就是限流器,connectionTimeout 已经给了排队语义,外面再加一层信号量容易出现两层超时叠加、出问题时分不清是被谁卡的。Semaphore 更适合下游 HTTP/RPC 这种没有池兜底的调用,那个场景它确实比线程池语义干净。

模式匹配补一个省事写法:case null, default -> 可以把空值和兜底合并成一个分支,比分开写短。sealed 全覆盖不写 default 这点提醒得好,编译器强制在重构时是实打实的安全网。

最后,别把 StructuredTaskScope 和虚拟线程一起当正式特性用——21 里它还是 preview,要 --enable-preview。断掉的代码块我已经补全重发了,谢了。