JDK 21 正式特性全面体验
大家好,我是尘光,持续学习,持续创作中
序言
2023 年 9 月 19 日,Java 21 发布了!这篇文章通过示例来全面体验一下 Java 21 的正式特性
这是自 2018 年 Java 10 发布以来,6 个月一个版本的第 12 个发行版。此外,这是继 Java 17 之后的又一个长期支持版本,Oracle 宣称至少会维护 8 年
新特性概览
Java 21 共有 15 个新特性,6 个预览版特性 + 1 个处于孵化阶段特性 + 8 个正式特性
JEP 编号 | 特性中文名 | 特性英文名 | 特性阶段 | 分类 |
---|---|---|---|---|
440 | Record 模式 | Record Patterns | 正式 | Amber 项目 |
441 | switch 模式匹配 | Pattern Matching for switch | 正式 | Amber 项目 |
430 | 字符串模板 | String Templates | 预览 | Amber 项目 |
443 | 匿名模式和变量 | Unnamed Pattern and Variable | 预览 | Amber 项目 |
445 | 匿名类和方法 | Unnamed Classes and Instance Main Methods | 预览 | Amber 项目 |
444 | 虚拟线程 | Virtual Threads | 正式 | Loom 项目 |
446 | 范围值 | Scoped Values | 预览 | Loom 项目 |
453 | 结构化并发 | Structured Concurrency | 预览 | Loom 项目 |
442 | 外部函数和内存 API | Foreign Function & Memory API | 预览 3 | Panama 项目 |
448 | 向量 API | Vector API | 孵化 6 | Panama 项目 |
431 | 有序集合接口 | Sequenced Collections | 正式 | Core |
439 | 分代 ZGC | Generational ZGC | 正式 | 性能提升 |
452 | 密钥封装机制 API | Key Encapsulation Mechanism API | 正式 | 性能提升 |
449 | 废弃 32 位 x86 架构 Windows 移植,待移除 | Deprecate the 32-bit x86 Port for Removal | 正式 | Other |
451 | 准备禁用代理的动态加载 | Prepare to Disallow the Dynamic Loading of Agents | 正式 | Other |
正式特性详解及代码实战
下载 IDEA 社区版最新版 2024.1 或 2024.2,下载 JDK 21(PS: 一台电脑上可以同时安装专业版和社区版)
在 IDEA 中新建一个工程 jdk-feature,选择已经下载好的 JDK21,Language Level 选择 21
JEP 431 - 有序集合接口
特性介绍
简而言之,在集合框架中增加一层描述顺序的抽象接口
在集合框架中新增了 SequencedCollection
, SequencedSet
, SequencedMap
接口
在此之前,Collection
的核心子接口是 Set
, List
和 Queue
,但对于 SortedSet
和 LinkedHashSet
来说,其实是有顺序的,和 List
接口有一些类似。此外,Deque
和 List
中的元素有序,并且都支持从两端操作和遍历。但是他们的顶层父接口都是 Collection
,从设计的角度来说,中间缺少一层抽象
SequencedCollection
定义的方法如下
public interface SequencedCollection<E> extends Collection<E> {
// 新引入的方法
SequencedCollection<E> reversed();
// 从 Deque 迁移的方法
void addFirst(E);
void addLast(E);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
}
可以看到,主要是定义了在两端添加、删除元素,获取两端的元素,以及 reversed()
方法。reversed()
方法返回原始集合从后往前结构的视图,修改原始集合是否对视图可见要取决于具体实现
同理,SequencedMap
也定义了从两端添加、删除元素,获取两端元素的方法
public interface SequencedMap<K,V> extends Map<K,V> {
// 新引入的方法
SequencedMap<K,V> reversed();
SequencedSet<K> sequencedKeySet();
SequencedCollection<V> sequencedValues();
SequencedSet<Entry<K,V>> sequencedEntrySet();
V putFirst(K, V);
V putLast(K, V);
// 从 NavigableMap 迁移的方法
Entry<K, V> firstEntry();
Entry<K, V> lastEntry();
Entry<K, V> pollFirstEntry();
Entry<K, V> pollLastEntry();
}
Java 21 实现的 4 个默认顺序集合接口视图如下
- ReverseOrderDequeView
- ReverseOrderListView
- ReverseOrderSortedMapView
- ReverseOrderSortedSetView
实战示例
实例化一个 ArrayList
后立即创建对应的 reverseOrder 视图,然后在原始集合上操作,这些操作会反映在视图上,最终倒序打印结果
SequencedCollection<Integer> list = new ArrayList<>(List.of(3, 6, 9, 12));
SequencedCollection<Integer> reversed = list.reversed();
list.removeFirst();
list.addLast(15);
// class java.util.ArrayList
System.out.println(list.getClass());
// class java.util.ReverseOrderListView$Rand
System.out.println(reversed.getClass());
// 15 12 9 6
reversed.forEach(System.out::println);
同样地,实例化一个 LinkedHashSet
后立即创建对应的 reverseOrder 视图,然后在原始集合上操作,这些操作同样会反映在视图上,最终倒序打印结果
SequencedSet<Integer> set = new LinkedHashSet<>();
SequencedSet<Integer> reversed = set.reversed();
set.addLast(3);
set.addLast(6);
set.addLast(9);
set.addLast(12);
set.removeFirst();
// class java.util.LinkedHashSet
System.out.println(set.getClass());
// class java.util.LinkedHashSet$1ReverseLinkedHashSetView
System.out.println(reversed.getClass());
// 12, 9, 6
reversed.forEach(System.out::println);
同样地,实例化一个 LinkedHashMap
后立即创建对应的 reverseOrder 视图,然后在原始集合上操作,这些操作同样会反映在视图上,最终倒序打印结果
SequencedMap<Integer, Integer> map = new LinkedHashMap<>();
SequencedMap<Integer, Integer> reversed = map.reversed();
map.putLast(3, 1);
map.putLast(6, 2);
map.putLast(9, 3);
map.putLast(12, 4);
map.pollFirstEntry();
// class java.util.LinkedHashMap
System.out.println(map.getClass());
// class java.util.LinkedHashMap$ReversedLinkedHashMapView
System.out.println(reversed.getClass());
// 12:4, 9:3, 6:2
reversed.forEach((k, v) -> System.out.println(k + ":" + v));
JEP 439 - 分代 ZGC
特性介绍
简而言之,ZGC 支持分代收集,更频繁地收集新生代对象
ZGC 在 Java 11 作为实验特性,在 Java 15 作为正式特性,标志着 ZGC 已经在生产环境可用了
ZGC 拥有极致的性能,有几个重要特性
- 并发收集,并且使用了染色指针、读屏障等技术
- 低延迟,承诺停顿时间小于
1ms
(刚开始引入时是10ms
) - 基于 Region,支持的堆内存范围是
16MB ~ 16TB
- 与 G1 相比,ZGC 对应用程序吞吐量的影响小于
15%
不过,在 Java 21 之前,ZGC 是不分代的,不区分新生代和老年代,都使用同一套回收算法。虽然整体性能很出色,但是仍然有性能优化的空间,因为新生代的对象存活时间较短,具有更高的回收价值
Java 21 对此做了性能优化,实现了 ZGC 的分代收集,更频繁地对新生代进行垃圾收集。在 ZGC 的基础上,分代 ZGC 有以下新特性
- 引入了写屏障技术
- 分配停顿风险更小
- 堆内存开销更小
- GC CPU 开销更小
目前,分代 ZGC 是更好的解决方案,适用于绝大多数场景。在后续的版本中,非分代 ZGC 会逐渐废弃,以减少维护成本。由于直接实现分代 ZGC 技术难度较大,而承诺产品发布周期比较紧凑,因此先使用非分代 ZGC 作为过渡版本。这是一个重要的产品思维:「先有功能,再做优化」
实战示例
准备工作
可以在 IDEA 菜单栏 Run/Debug Configurations 中配置 JVM 参数
由于要观察 JVM 内存变化情况,可以下载 VisualVM,打开后依次点击菜单栏 Tools / Plugins 下载 Visual GC 插件
以下代码构造了 SmallObject
和 HugeObject
,循环 30000 次,共创建 30000 个 SmallObject
对象,6 个 HugeObject
对象,大对象保存到静态容器 HUGE_CACHE
中,防止被回收
public class Jep439Demo {
private static final List<HugeObject> HUGE_CACHE = new ArrayList<>();
static class SmallObject {
String name;
byte[] arr;
public SmallObject(String name) {
this.name = name;
this.arr = new byte[16 * 1024];
for (int i = 0; i < this.arr.length; i++) {
this.arr[i] = (byte) (i % 256);
}
}
public String getName() {
return name;
}
}
static class HugeObject {
byte[] arr;
public HugeObject() {
this.arr = new byte[3 * 1024 * 1024];
for (int i = 0; i < this.arr.length; i++) {
this.arr[i] = (byte) (i % 256);
}
}
}
public static void main(String[] args) throws Exception {
// Wait for monitoring in VisualVM.
Thread.sleep(20_000);
List<String> list = new ArrayList<>();
Random random = new Random();
for (int i = 1; i <= 30000; i++) {
SmallObject item = new SmallObject("obj" + i);
list.add(item.getName());
if (i % 5000 == 0) {
HUGE_CACHE.add(new HugeObject());
}
if (i % 100 == 0) {
System.out.printf("Round %s, small.size()=%s, huge.size()=%s%n", i, list.size(), HUGE_CACHE.size());
}
Thread.sleep(10 + random.nextInt(10));
}
// Waiting for 12 seconds.
Thread.sleep(12_000);
}
}
由于 VisualVM 里的 Visual GC 插件暂时不支持 ZGC,直接分析 GC 日志文件
ZGC - 开启分代
配置 JVM 参数,运行并在 VisualVM 中监控
-Xmx100m
-Xlog:gc:file=/Users/jason315/Desktop/gc/zgc-gen.log
-Xlog:gc*:file=/Users/jason315/Desktop/gc/zgc-gen-detail.log
-XX:+UseZGC
-XX:+ZGenerational
相对于非分代 ZGC,特殊的参数是 -XX:+ZGenerational
,表示开启分代模式。这里没有添加 JVM 堆的初始大小参数 -Xms100m
,是为了观察 ZGC 的一个重要特性:内存自动整理
VisualVM 统计的内存趋势图如下。程序结束前,已用的 JVM 内存为 45M
左右。可以看到,当 ZGC 分析出前一段时间的内存使用率较低时,主动降低了堆内存,之后再根据已用内存自动调整堆内存
重要 GC 日志如下,算上预热,共有
66
次 GC,GC 总时间为 3215ms
,其中新生代总时间为 1161ms
,老年代总时间为 2054ms
通过分析 GC 日志可以看出,每次 GC 时,先打印了
GC(n) Y: xxx
日志,然后打印了 GC(n) O: xxx
日志,同时触发了新生代和老年代垃圾收集
程序退出前,打印了 ZGC 统计信息,从中摘取了新生代和老年代停顿时间信息,以最坏的情况考虑,每次 GC 时,停顿时间都远远小于 1ms
[535.801s][info][gc,stats ] Last 10s Last 10m Last 10h Total
[535.801s][info][gc,stats ] Avg / Max Avg / Max Avg / Max Avg / Max
[535.801s][info][gc,stats ] Old Pause: Pause Mark End 0.000 / 0.000 0.010 / 0.020 0.010 / 0.020 0.010 / 0.020 ms
[535.801s][info][gc,stats ] Old Pause: Pause Relocate Start 0.000 / 0.000 0.008 / 0.019 0.008 / 0.019 0.008 / 0.019 ms
[535.802s][info][gc,stats ] Young Pause: Pause Mark End 0.000 / 0.000 0.014 / 0.023 0.014 / 0.023 0.014 / 0.023 ms
[535.802s][info][gc,stats ] Young Pause: Pause Mark Start 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 ms
[535.802s][info][gc,stats ] Young Pause: Pause Mark Start (Major) 0.000 / 0.000 0.018 / 0.066 0.018 / 0.066 0.018 / 0.066 ms
[535.802s][info][gc,stats ] Young Pause: Pause Relocate Start 0.000 / 0.000 0.012 / 0.094 0.012 / 0.094 0.012 / 0.094 ms
ZGC - 不开启分代
配置 JVM 参数,运行并在 VisualVM 中监控
-Xmx100m
-Xlog:gc:file=/Users/jason315/Desktop/gc/zgc.log
-Xlog:gc*:file=/Users/jason315/Desktop/gc/zgc-detail.log
-XX:+UseZGC
同样地,这里没有添加 JVM 堆的初始大小参数 -Xms100m
,是为了观察 ZGC 的一个重要特性:内存自动整理
VisualVM 统计的内存趋势图如下。程序结束前,已用的 JVM 内存为 40M
左右。可以看到,当 ZGC 分析出前一段时间的内存使用率较低时,主动降低了堆内存,之后再根据已用内存自动调整堆内存。可以看出,已用内存趋势图要平滑一些
重要 GC 日志如下,算上预热,共有
62
次 GC
同样地,从中摘取停顿时间信息,以最坏的情况考虑,每次 GC 时,停顿时间也远远小于
1ms
[550.055s][info][gc,stats ] Last 10s Last 10m Last 10h Total
[550.055s][info][gc,stats ] Avg / Max Avg / Max Avg / Max Avg / Max
[550.055s][info][gc,stats ] Phase: Pause Mark End 0.011 / 0.011 0.014 / 0.065 0.014 / 0.065 0.014 / 0.065 ms
[550.055s][info][gc,stats ] Phase: Pause Mark Start 0.009 / 0.009 0.010 / 0.065 0.010 / 0.065 0.010 / 0.065 ms
[550.055s][info][gc,stats ] Phase: Pause Relocate Start 0.005 / 0.005 0.006 / 0.011 0.006 / 0.011 0.006 / 0.011 ms
[550.055s][info][gc,stats ] Subphase: Pause Mark Try Complete 0.000 / 0.000 0.001 / 0.001 0.001 / 0.001 0.001 / 0.001 ms
G1
配置 JVM 参数,运行并在 VisualVM 中监控
-Xmx100m
-Xlog:gc:file=/Users/jason315/Desktop/gc/g1.log
-Xlog:gc*:file=/Users/jason315/Desktop/gc/g1-detail.log
-XX:+UseG1GC
VisualVM 统计的内存趋势图如下。程序结束前,已用的 JVM 内存为 55M
左右,峰值内存达到了 80+M
,相比于 ZGC,变化幅度较大
Visual GC 统计信息如下,算上预热,共有
10
次 GC,GC 总时间为 191.213ms
最后一次 GC 日志,可以看出,停顿时间为
18.754ms
,比 ZGC 差了不止一个量级
JEP 440 - Record 模式
特性介绍
简而言之,instanceof 关键字后支持 Records,简化 Records 对象值和方法的访问
Java 16 引入了一个正式特性: Records,表示一种特殊的类,极大地简化了不可变类的定义。如以下代码所示,Point
类有 2 个 final
属性,分别为 x
和 y
record Point(int x, int y) {}
Java 16 还扩展了 instanceof
关键字,引入 Type 模式,在 if 语句块内使用变量时,不用强制类型转换了
public static void testInstanceOf(Object o) {
// Java 16 以前
if (o instanceof String) {
String str = (String) o;
// ... 强制类型转换后才能使用 str ...
}
// Java 16 及以上
if (o instanceof String str) {
// ... 直接使用 str ...
}
}
JEP440 扩展了模式匹配以表示更复杂,组合更灵活的数据访问方式,极大提高了开发效率
Record 模式增强了 Java 编程语言以便于解构 Record 对象的值,这可以嵌套 Record 模式和 Type 模式,实现强大的、声明式的和可组合的数据处理形式
Record 模式可以和 switch 模式匹配结合使用,会在接下来的 JEP441 中介绍
实战示例
来自 JEP440 的示例如下,很明显 printSumInJava21
的写法更简洁。输出结果均为 9
public class Jep440Demo {
record Point(int x, int y) {}
// Java 16 写法
static void printSumInJava16(Object obj) {
if (obj instanceof Point p) {
int x = p.x();
int y = p.y();
System.out.println(x+y);
}
}
// Java 21 写法
static void printSumInJava21(Object obj) {
if (obj instanceof Point(int x, int y)) {
System.out.println(x+y);
}
}
public static void main(String[] args) {
Point point = new Point(3, 6);
printSumInJava16(point);
printSumInJava21(point);
}
}
同样地,Record 模式支持嵌套的 Records 解构赋值
public class Jep440NestedDemo {
record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}
static void printRectangleInfoInJava21(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
int area = Math.abs(lr.p().x() - ul.p().x()) * Math.abs(lr.p().y() - ul.p().y());
String template = "Rectangle upper-left color is %s, lower-right color is %s.";
System.out.printf((template) + "%n", ul.c(), lr.c());
System.out.printf("Rectangle area is %d.", area);
}
}
public static void main(String[] args) {
Point ulPoint = new Point(0, 6);
ColoredPoint ulColorPoint = new ColoredPoint(ulPoint, Color.RED);
Point lrPoint = new Point(12, 0);
ColoredPoint lrColorPoint = new ColoredPoint(lrPoint, Color.BLUE);
Rectangle rectangle = new Rectangle(ulColorPoint, lrColorPoint);
printRectangleInfoInJava21(rectangle);
}
}
执行结果如下
Rectangle upper-left color is RED, lower-right color is BLUE.
Rectangle area is 72.
JEP 441 - switch 模式匹配
特性介绍
简而言之,switch 表达式支持更多的类型,并且支持模式匹配,自动解构赋值
经过 4 次预览后,switch 模式匹配终于在 Java 21 成为正式特性。从此,Java switch 语法更加完善,更加易用
在之前的 4 次预览中已经介绍过,switch 表达式的核心语法如下
-
增强类型检查,case 表达式支持更多类型,包括 Records
对于有父子关系的多个子句,如果父类型在子类型之前,父类型子句会优先匹配,子类型子句将不可达,会抛出编译期错误。此时应该调换一下顺序
-
switch 表达式和语句分支全覆盖检测
-
扩展了模式变量声明范围
- 任意的 when 语句
- case 语句箭头后的表达式、代码块、throw 语句
- 一个 case 语句的模式变量范围,不允许越过另一个 case 语句
-
优化 null 处理,可以声明一个 null case
在 JEP441 中,与前面的预览 JEP 相比,有 2 个新变化
- 去掉了括号模式,因为不存在有效值,无使用场景
- case 语句支持限定枚举常量
实战示例
JEP441 中提供的一个示例
public class Jep441Demo {
record Point(int i, int j) {}
enum Color { RED, GREEN, BLUE; }
static void typeTester(Object obj) {
switch (obj) {
// case 语句中支持 null
case null -> System.out.println("null");
// case 语句中支持模式匹配,在后面的语句中可以直接使用 s
case String s -> System.out.println("String is: " + s);
// case 语句支持枚举模式匹配
case Color c -> System.out.println("Color: " + c.toString());
// case 语句支持 Record 模式,前面已经介绍过
case Point p -> System.out.println("Record class: " + p.toString());
case int[] ia -> System.out.println("Array length" + ia.length);
default -> System.out.println("Something else");
}
}
}
通常,一个 case 分支中还会细分不同的处理逻辑
// 不推荐写法
static void testStringOld(String response) {
switch (response) {
case null -> { }
case String s -> {
if (s.equalsIgnoreCase("YES"))
System.out.println("You got it");
else if (s.equalsIgnoreCase("NO"))
System.out.println("Shame");
else
System.out.println("Sorry?");
}
}
}
// 推荐写法,可读性更强
static void testStringNew(String response) {
switch (response) {
case null -> { }
case String s when s.equalsIgnoreCase("YES") -> System.out.println("You got it");
case String s when s.equalsIgnoreCase("NO") -> System.out.println("Shame");
case String s -> System.out.println("Sorry?");
}
}
Java 21 中的 case 语句支持枚举常量,如以下代码所示
public class Jep441EnumDemo {
sealed interface CardClassification permits Suit, Tarot {}
public enum Suit implements CardClassification { CLUBS, DIAMONDS, HEARTS, SPADES }
static final class Tarot implements CardClassification {}
// Java 21 之前
static void exhaustiveSwitchWithoutEnumSupport(CardClassification c) {
switch (c) {
case Suit s when s == Suit.CLUBS -> System.out.println("It's clubs");
case Suit s when s == Suit.DIAMONDS -> System.out.println("It's diamonds");
case Suit s when s == Suit.HEARTS -> System.out.println("It's hearts");
case Suit s -> System.out.println("It's spades");
case Tarot t -> System.out.println("It's a tarot");
}
}
// Java 21 及以后
static void exhaustiveSwitchWithBetterEnumSupport(CardClassification c) {
switch (c) {
case Suit.CLUBS -> System.out.println("It's clubs");
case Suit.DIAMONDS -> System.out.println("It's diamonds");
case Suit.HEARTS -> System.out.println("It's hearts");
case Suit.SPADES -> System.out.println("It's spades");
case Tarot t -> System.out.println("It's a tarot");
}
}
}
JEP 444 - 虚拟线程
特性介绍
简而言之,Java 正式支持轻量级线程
虚拟线程无疑是 Java 21 最受关注的正式特性。虚拟线程,也就是轻量级线程。虚拟线程极大地降低了高吞吐量应用的开发和维护成本
平台线程(原有线程)是在 OS 线程上做的封装,它的创建和切换成本很高,可用的线程数量也有限制。对于并发较高的应用,想要提高系统的吞吐量,之前一般是做异步化,但这种方式很难定位线上问题
虚拟线程的资源分配和调度由 Java 平台实现,它不再直接与 OS 线程强关联,而是直接将平台线程作为载体线程,这使得虚拟线程的可用数量大大增加
每个请求一个线程
虚拟线程的引入,让「每个请求一个线程」风格再次回到开发者的视线。这是一种常见的 Web 应用程序架构模式,用于处理并发请求。在这种模式下,每个传入的请求(如 HTTP 请求)都会分配一个独立的线程来处理,直到该请求完成为止。一旦请求处理完成,线程就会释放,可以被用于处理其他请求
这种模式的主要优点包括:
-
逻辑简单
每个请求的处理逻辑都在自己的线程中运行,避免了复杂的并发控制和同步问题
-
快速响应
由于每个请求都在单独的线程中处理,因此可以快速响应请求,而不受其他请求的影响
-
易于开发
开发者可以按照顺序编程的方式编写代码,而不必担心线程安全问题
虚拟线程应用场景
虚拟线程在如下两种场景下,才能大幅提高应用系统的吞吐量
- 并发任务量很大(万级)
- 线程工作量不会使 CPU 受限(不是 CPU 密集型任务)
虚拟线程创建方式
共有 5 中创建虚拟线程的方式
class Task implements Runnable{
@Override
public void run() {
Thread currentThread = Thread.currentThread();
long threadId = currentThread.threadId();
System.out.printf("[%d]Task start to run...\n", threadId);
int sum = IntStream.range(0, 100).sum();
System.out.printf("[%d]sum=%d, 线程名称: '%s', 是否虚拟线程: %s\n",
threadId, sum, currentThread.getName(), currentThread.isVirtual());
System.out.printf("[%d]Task end.\n", threadId);
}
}
// 方式一:直接启动,虚拟线程名称为空
private void way1() {
// 等价于 Thread.ofVirtual().start(new Task());
Thread.startVirtualThread(new Task());
}
// 方式二:Builder 模式构建
private void way2() {
Thread vt = Thread.ofVirtual()
.name("VirtualWorker-", 1)
.unstarted(new Task());
vt.start();
}
// 方式三:Factory 模式构建
private void way3() {
ThreadFactory factory = Thread.ofVirtual()
.name("VirtualFactoryWorker-", 1)
.factory();
Thread vt = factory.newThread(new Task());
vt.start();
}
// 方式四:newVirtualThreadPerTaskExecutor
private void way4() {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(new Task());
}
}
// 方式五:构建"虚拟线程池"
private void way5() {
ThreadFactory factory = Thread.ofVirtual()
.name("VirtualFactoryWorker-", 1)
.factory();
ExecutorService executorService = Executors.newThreadPerTaskExecutor(factory);
executorService.submit(new Task());
}
实战示例
示例 1
基于上述创建虚拟线程的 5 中方法,进行如下测试
public class Jep444CreateWaysDemo {
private static final List<String> LOGS = new CopyOnWriteArrayList<>();
static class Task implements Runnable{
@Override
public void run() {
Thread currentThread = Thread.currentThread();
long threadId = currentThread.threadId();
LOGS.add("[%d]Step 1, task start to run...".formatted(threadId));
int sum = IntStream.range(0, 100).sum();
LOGS.add("[%d]Step 2, sum=%d, 线程名称: '%s', 是否虚拟线程: %s".formatted(threadId, sum, currentThread.getName(), currentThread.isVirtual()));
LOGS.add("[%d]Step 3, task end.".formatted(threadId));
}
}
// 方式一:直接启动,虚拟线程名称为空
private static void way1() {
// 等价于 Thread.ofVirtual().start(new Task());
Thread.startVirtualThread(new Task());
}
// 方式二:Builder 模式构建
private static void way2() {
Thread vt = Thread.ofVirtual()
.name("VirtualWorker-", 1)
.unstarted(new Task());
vt.start();
}
// 方式三:Factory 模式构建
private static void way3() {
ThreadFactory factory = Thread.ofVirtual()
.name("VirtualFactoryWorker-", 1)
.factory();
Thread vt = factory.newThread(new Task());
vt.start();
}
// 方式四:newVirtualThreadPerTaskExecutor
private static void way4() {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(new Task());
}
}
// 方式五:构建"虚拟线程池"
private static void way5() {
ThreadFactory factory = Thread.ofVirtual()
.name("VirtualFactoryWorker-", 1)
.factory();
try (ExecutorService executorService = Executors.newThreadPerTaskExecutor(factory)) {
executorService.submit(new Task());
}
}
public static void main(String[] args) throws Exception {
way1();
way2();
way3();
way4();
way5();
Thread.sleep(5_000);
// 由于多线程之间是无序的,对结果进行排序,方便展示
LOGS.stream().sorted().forEach(System.out::println);
}
}
运行结果如下,结果显示线程都是虚拟线程
[21]Step 1, task start to run...
[21]Step 2, sum=4950, 线程名称: '', 是否虚拟线程: true
[21]Step 3, task end.
[23]Step 1, task start to run...
[23]Step 2, sum=4950, 线程名称: 'VirtualWorker-1', 是否虚拟线程: true
[23]Step 3, task end.
[25]Step 1, task start to run...
[25]Step 2, sum=4950, 线程名称: 'VirtualFactoryWorker-1', 是否虚拟线程: true
[25]Step 3, task end.
[27]Step 1, task start to run...
[27]Step 2, sum=4950, 线程名称: '', 是否虚拟线程: true
[27]Step 3, task end.
[31]Step 1, task start to run...
[31]Step 2, sum=4950, 线程名称: 'VirtualFactoryWorker-1', 是否虚拟线程: true
[31]Step 3, task end.
示例 2
创建 100 万个虚拟线程
public class Jep444HugeThreadsDemo {
private static void createHugeThreads() {
// 创建 100 万个虚拟线程
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 1000_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
}); // try-with-resources,会隐式调用 executor.close()
}
}
public static void main(String[] args) throws Exception {
// Wait for VisualVM to monitor.
Thread.sleep(10_000);
System.out.println("Start to create virtual threads...");
long startTime = System.currentTimeMillis();
createHugeThreads();
System.out.printf("Create huge virtual threads finished, cost %d ms.", System.currentTimeMillis() - startTime);
// Wait for 5 seconds.
Thread.sleep(5_000);
}
}
配置 JVM 参数后执行,并且在 VisualVM 中观察
-Xmx3072m
-XX:+UseZGC
-XX:+ZGenerational
运行结果如下,可以很轻松地创建 100 个虚拟线程
Start to create virtual threads...
Create huge virtual threads finished, cost 7395 ms.
VisualVM 统计信息如下,最大堆内存不到 1250M
其中有一个 VirtualThread-unparker 线程较为特殊
JEP 449 - 废弃 32 位 x86 架构 Windows 移植
简而言之,准备不支持在 32 位 x86 架构 Windows 上运行了
随着计算机硬件的飞速发展,64 位系统逐渐普及。支持 32 位的最后一个 Windows 操作系统,Windows 10 也仅会支持到 2025 年 10 月
基于以上原因,Java 21 宣布废弃对 32 位 x86 架构 Windows 的移植,并在以后的版本中彻底移除(JEP479)。这样一来,丢下了一些包袱,OpenJDK 社区将腾出更多的精力专注于开发其他功能。在此之前,实现虚拟线程时还对此做了兼容
当尝试在 32 位 x86 架构的 Windows 上使用 Java 21 编译时,默认会抛出如下提示信息
bash ./configure
...
checking compilation type... native
configure: error: The Windows 32-bit x86 port is deprecated and may be removed in a future release. \
Use --enable-deprecated-ports=yes to suppress this error.
configure exiting with result code 1
可以使用 --enable-deprecated-ports=yes
继续编译
bash ./configure --enable-deprecated-ports=yes
幸运的是,历史 JDK 版本仍然支持 32 位 x86 架构 Windows 系统,不会受到影响
JEP 451 - 准备禁用代理的动态加载
简而言之,代理的动态加载时,发出警告
当代理在运行中的 JVM 中动态加载时,会发出警告。这些警告的目的是让用户为将来的版本做好准备,将来的版本默认不允许动态加载代理,以提高 Java 安全性
在任何版本中,Java 自带的工具在启动时加载代理不会发出警告,如 jcmd
和 jconsole
JEP 452 - 密钥封装机制 API
特性介绍
简而言之,Java 提供了 KEM API
在计算机安全领域,KEM(Key Encapsulation Mechanism,密钥封装机制)是一种加密技术,它允许两个通信方安全地共享一个秘密密钥,而无需通过不安全的通道直接传输密钥。KEM 通常与对称加密算法结合使用,以实现高效的数据加密和安全通信
加密时,传统方法是使用公钥随机生成一个对称密钥,但这需要填充,也难以保证安全性。而密钥封装机制(KEM)则利用公钥的属性派生出一个相关的对称密钥,这种方法不需要填充
Java 平台传统的加密方式无法抵御量子攻击,而 KEM API 则可以有效弥补这个短板
KEM 的三个函数
KEM 算法由以下 3 个函数组成
-
密钥对生成函数
返回包含公钥和私钥的密钥对,由已有的
KeyPairGenerator API
提供 -
密钥封装函数
sender 调用密钥封装函数,根据 receiver 的公钥和封装选项,生成密钥
K
,以及一个封装消息,然后 sender 将这个封装消息发送给 receiver -
密钥解封装函数
receiver 接收到封装消息后,使用自己的私钥解密,得到密钥
K
KEM API
KEM API 相关的接口和类都位于 javax.crypto
包下
Java 21 新增了 KEMSpi
(KEM Service Provider Interface),其中包括 EncapsulatorSpi
和 DecapsulatorSpi
。Java 提供的默认实现是 DHKEM
,即 Diffie-Hellman KEM
public interface KEMSpi {
EncapsulatorSpi engineNewEncapsulator(PublicKey var1, AlgorithmParameterSpec var2, SecureRandom var3) throws InvalidAlgorithmParameterException, InvalidKeyException;
DecapsulatorSpi engineNewDecapsulator(PrivateKey var1, AlgorithmParameterSpec var2) throws InvalidAlgorithmParameterException, InvalidKeyException;
public interface DecapsulatorSpi {
SecretKey engineDecapsulate(byte[] var1, int var2, int var3, String var4) throws DecapsulateException;
int engineSecretSize();
int engineEncapsulationSize();
}
public interface EncapsulatorSpi {
KEM.Encapsulated engineEncapsulate(int var1, int var2, String var3);
int engineSecretSize();
int engineEncapsulationSize();
}
}
此外,还新增了 KEM
类
小结
Java 21 是一个重要版本,尤其是分代 ZGC 和虚拟线程,其他语言层面的改进也值得关注。作为长期支持版本,值得在实际项目中逐步尝试升级,它将不负众望
参考文档
JEP 431 - Sequenced Collections
JEP 441 - Pattern Matching for switch
JEP 449 - Deprecate the 32-bit x86 Port for Removal
JEP 451 - To Disallow the Dynamic Loading of Agents
转载自:https://juejin.cn/post/7383311950174339110