JDK21 核心新特性
一、重磅稳定正式特性(生产可直接使用)
1. JEP444 虚拟线程 Virtual Threads(最核心、变革并发编程)
底层原理
传统平台线程:1:1 绑定操作系统内核线程,每个线程栈默认 1MB,数量上限几千,线程池需要反复调参(核心线程、队列、拒绝策略)。 虚拟线程:JVM 自研 M:N 调度,由载体线程(Carrier Thread) 复用 OS 线程;虚拟线程栈极小(几百字节),可轻松创建百万级线程。 当虚拟线程发生 IO 阻塞(DB、Redis、HTTP、sleep),会自动卸载、归还载体线程给其他虚拟线程使用,不会占死 OS 线程。
// 1. 快速创建单个虚拟线程
Thread.startVirtualThread(() -> System.out.println("虚拟线程任务"));
// 2. 生产推荐:每个任务单独分配虚拟线程(无固定线程池大小限制)
try (ExecutorService vtExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
// 提交百万任务无OOM风险,传统ThreadPoolExecutor会直接打满队列触发拒绝策略
for (int i = 0; i < 1_000_000; i++) {
vtExecutor.submit(() -> {
// 业务:调用Feign、查询数据库、抽帧任务等IO密集操作
});
}
} // try-with-resources自动等待全部任务完成,自动关闭
对业务的价值
- 不用手动配置
corePoolSize/maxPoolSize/queue,省去线程池调优成本; - IO 密集型任务吞吐量大幅提升;
- 天然适配 "一请求一线程",不用异步回调、CompletableFuture 嵌套,代码同步写法更简单;
=================什么是 IO 密集型 / CPU 密集型=============================
- IO 密集型
程序大部分时间卡在等待外部资源,CPU 空闲没事干:
- 网络 IO:RTSP 拉流、Feign 调工作流、HTTP 请求大模型、Redis、MySQL 查询
- 文件 IO:FFmpeg 读写视频、MinIO 上传图片、本地文件读写
线程发起 IO 请求后,会阻塞等待返回,这期间 CPU 完全闲置。
2.CPU 密集型
大量复杂计算、循环、编解码运算,CPU 全程满载,几乎无等待。
比如:本地视频解码、超大报表计算、复杂数学运算。
传统平台线程池为什么不适合 IO 密集?
传统 Java 线程是操作系统内核线程 1:1 映射:
-
每个线程占用固定栈内存(默认 1MB),操作系统能承载的线程上限很低,一般几千条顶天;
-
线程阻塞在 IO 时,内核线程会被占死,无法复用;
举个例子: 你开 200 个线程池,200 个任务同时请求大模型接口,全部阻塞等待响应。 此时 200 个 OS 线程全部卡死,哪怕 CPU 空闲,也没有多余线程处理新的任务,吞吐量卡死。 想要提高并发,只能疯狂调大线程池 max,线程越多内存占用越高,容易 OOM。
JDK21 虚拟线程为什么对 IO 密集型极度友好?
核心原理:M:N 调度,IO 阻塞自动释放载体线程
-
虚拟线程本身极轻量,栈内存极小,支持创建百万级;
-
虚拟线程阻塞在网络 / DB IO 时,JVM 会自动把它挂起,载体线程释放出来给其他虚拟线程使用; 载体线程只是少量固定的平台线程,不断复用。
为什么 CPU 密集型不适合虚拟线程?
如果是纯计算场景,虚拟线程不会阻塞,会一直占用载体线程疯狂运算,载体线程被钉死无法复用,此时虚拟线程没有任何优势,和普通线程池性能差别不大。
【前置知识】
载体线程(Carrier Thread)
本质就是普通平台线程(OS 内核线程),由 JVM 内部维护少量固定数量,相当于工人。
JDK 虚拟线程底层用 ForkJoinPool 作为载体线程池,默认载体数量 = CPU 核心数。
虚拟线程(Virtual Thread)
JVM 层面轻量化逻辑线程,不直接绑定 OS 线程,只是一段任务逻辑,本身不占用操作系统资源,可创建百万个。
核心关系:M:N 映射
N 个虚拟线程,复用 M 个载体线程(M 远小于 N)。
同一时刻,一个载体线程只能执行一个虚拟线程。
大白话举例
载体线程 = 食堂厨师(只有 4 个)
虚拟线程 = 待处理的任务(几千上万个)
- 任务 A 正在厨师 1 手上处理,中途要等外部 AI 接口返回(IO 阻塞);
- 厨师 1 不用干等,放下任务 A(挂起虚拟线程),去处理任务 B;
- 等 AI 接口数据回来,任务 A 再排队,有空厨师再接着处理。
传统线程对比: 传统平台线程执行 IO 阻塞,厨师会卡死在这个任务上,啥也干不了,只能新增更多厨师(线程)。
IO 完成后如何恢复?
当操作系统返回 IO 数据(网络 / 数据库响应):
- JVM 收到 IO 就绪通知;
- 该虚拟线程重新进入就绪队列;
- 载体线程空闲时,再次绑定它,从阻塞处继续往下执行剩余业务代码。
补充两个关键点
(1)什么时候载体线程不会释放?
如果使用 synchronized 同步块锁住代码,虚拟线程阻塞在锁竞争时,不会解绑载体线程,载体线程被占用无法复用,会丧失虚拟线程优势。
(2)CPU 密集场景无复用效果
如果虚拟线程全是纯计算,无任何 IO 阻塞,虚拟线程会一直占用载体线程跑计算,载体无法释放,此时虚拟线程没有性能提升。
========================================================================
2. JEP441 Switch 模式匹配(转正,简化类型判断)
告别 **instanceof + 强转**样板代码,支持类型匹配、null 匹配、when 条件判断。
旧写法:
Object obj = "123";
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
JDK21 新 switch:
String res = switch (obj) {
case null -> "空值";
case Integer i when i > 100 -> "大数整数";
case String s -> "字符串长度:" + s.length();
default -> "其他类型";
};
3. JEP440 Record 记录模式(嵌套解构 Record)
配合 Record 类,直接解构内部字段,支持多层嵌套 Record 拆解。
record Point(int x, int y){}
record Line(Point start, Point end){}
Line line = new Line(new Point(1,2), new Point(3,4));
// 嵌套解构,直接拿到x1/y1/x2/y2
if(line instanceof Line(Point(int x1, int y1), Point(int x2, int y2))){
System.out.println(x1 + "," + y1);
}
4. JEP431 序列集合 Sequenced Collections(统一有序集合 API)
新增三套接口:SequencedCollection / SequencedSet / SequencedMap,List、LinkedHashSet、LinkedHashMap 自动实现,统一首尾操作、反转 API。
JDK17 及以前写法(繁琐易错):
List<String> list = List.of("A","B","C");
String first = list.get(0);
String last = list.get(list.size()-1);
List<String> reverse = new ArrayList<>(list);
Collections.reverse(reverse);
JDK21 简化:
SequencedCollection<String> seq = List.of("A","B","C");
seq.getFirst();
seq.getLast();
seq.reversed(); // 返回逆序视图
// LinkedHashMap 支持头部插入、弹出尾部(消息列表、最近访问缓存)
SequencedMap<String,Object> map = new LinkedHashMap<>();
map.putFirst("k1",v1);
map.pollLastEntry();
业务场景:最近操作记录、时序任务列表、有序缓存。
5. JEP439 分代 ZGC(Generational ZGC,正式稳定)
原 ZGC 是非分代,所有对象统一回收;JDK21 分代 ZGC 把堆分为年轻代、老年代:
- 短生命周期对象(临时请求、临时 DTO)在年轻代快速回收,减少老年代扫描;
- 停顿时间依旧亚毫秒(<10ms),吞吐量提升 20%+,内存占用降低 25%;
- 高并发、大量临时对象的业务(业务运行过程中只使用一次、用完就丢弃、生命周期极短的对象,执行完方法就失去引用,很快要被 GC 回收,这类就是临时对象。)GC 压力大幅下降。
为什么这类业务 GC 压力大(JDK17 及之前不分代 ZGC/G1)?
- 短命对象全部堆在一起,老年代、年轻代不区分;
- 大量临时对象塞满堆,频繁触发 Minor GC;
- GC 需要扫描全部堆内存标记存活对象,频繁 STW 停顿,批量任务、定时任务容易卡顿;
- 部分临时对象晋升到老年代,老年代慢慢填满,触发开销更大的 Full GC。
JDK21 分代 ZGC 怎么降低 GC 压力?
分代 ZGC 把堆拆成年轻代(新生代)+ 老年代:
- 所有临时小对象优先分配到年轻代;
- 年轻代回收速度极快,专门清理短命对象,扫描范围极小,停顿极低;
- 临时对象几乎不会晋升到老年代,老年代只存长期驻留数据(缓存、持久业务对象);
- 减少全堆扫描,GC 频率、单次 GC 耗时同步下降。
启动参数:
java -XX:+UseZGC -XX:+ZGenerational -Xmx8g -jar app.jar