Java 21 新功能介绍 (LTS)
Oracle JDK 下载:https://www.oracle.com/cn/java/technologies/downloads
OpenJDK 21 下载:https://jdk.java.net/21/
Java 21 引入了 14 个新内容:
JEP | 内容 | 分类 |
---|---|---|
JEP 444 | 虚拟线程 | 核心 Java 库 |
JEP 431 | 有序集合 | 核心 Java 库 |
JEP 442 | 外部函数和内存API(第三次预览) | 核心 Java 库 |
JEP 446 | 作用域值(预览) | 核心 Java 库 |
JEP 448 | Vector API(第六次孵化) | 核心 Java 库 |
JEP 453 | 结构化并发(预览) | 核心 Java 库 |
JEP 440 | Record 模式 | Java 语言规范 |
JEP 441 | switch模式匹配 | Java 语言规范 |
JEP 430 | 字符串模板(预览) | Java 语言规范 |
JEP 443 | 未命名模式的变量(预览) | Java 语言规范 |
JEP 445 | 未命名类和 main 方法(预览) | Java 语言规范 |
JEP 439 | 分代ZGC | HotSpot |
JEP 449 | 弃用Windows 32位x86端口 | HotSpot |
JEP 451 | 准备禁止动态加载代理 | HotSpot |
JEP 452 | 密钥封装机制API | 安全库 |
对于本文中的代码,如果是预览功能,运行时候都需要添加开启预览功能参数。
java --enable-preview --source 21 Xxx.java
JEP 444. 虚拟线程 ⭐️
虚拟线程是 Java 21 中最为重要的特性。Java 从 Java 19 开始引入虚拟线程,在 Java 21 中就正式升级为正式特性。可见官方也把虚拟线程作为 Java 21 长久支持版本的吸引点。虚拟线程是轻量级的线程,可以在显著的减少代码编写的同时提高系统的吞吐量。
注意:虚拟线程可以提高系统的吞吐量,不能提高运行速度,也不适用于 CPU 计算密集型任务
引入虚拟线程原因
一直以来,在 Java 并发编程中,Thread 都是十分重要的一部分,Thread 是 Java 中的并发单元,每个 Thread 线程都提供了一个堆栈来存储局部变量和方法调用,以及线程上下文等相关信息。
但问题是线程和进程一样,都是一项昂贵的资源,JDK 将 Thread 线程实现为操作系统线程的包装器,成本很高,而且数量有限。因此我们会使用线程池来管理线程,同时限制线程的数量。比如常用的 Tomcat 会为每次请求单独使用一个线程进行请求处理,同时限制处理请求的线程数量以防止线程过多而崩溃;这很有可能在 CPU 或网络连接没有耗尽之前,线程数量已经耗尽,从而限制了 web 服务的吞吐量。
可能有些同学要说了,那么可以放弃请求和线程一一对应的方式,使用异步编程来解决这个问题。把请求处理分段,在组合成顺序管道,通过一套 API 进行管理,这样就可以使用有限的线程来处理超过线程数量的请求。这当然也是可以的,但是随之而来的问题是:
- 需要额外的学习异步编程。
- 代码复杂度增加,等于放弃了语言的基本顺序组合运算。
- 堆栈上下文信息都变得难以追踪。
- Debug 困难。
- 和 Java 平台本身的编程风格有冲突,Java 并发单元是 Thread,而这时是异步管道。
而事实上,以上面的请求开启一个线程处理为例,因为 DB 查询速度过慢,请求量过大,可能导致我们的线程数量已经使用殆尽,新的请求将被阻塞,但是机器的性能尚有剩余剩余,性能浪费。
那么对于这种需要提高吞吐量的场景,使用虚拟线程将会大大改善这种情况。
虚拟线程的使用
这里我们不去介绍虚拟线程的实现原理,对开发者来说虚拟线程在使用体验上和 Thread 几乎没有区别,与之前的 API 互相兼容,但是相比之下虚拟线程资源占用非常少,虚拟线程是一种即用即启动的线程,不应该被池化存储。
创建提交执行虚拟线程
下面是一个示例,创建 1 万个线程,然后都休眠 1 秒钟结束线程,如果使用传统的 Thread 线程,可能会因为线程数量不够而直接异常。如果是线程池的方式,会基于线程池的线程数并发,那么剩余线程只能等待;但是使用虚拟线程的方式,可以瞬间完成。
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;
/**
* @author https://www.wdbyte.com
*/
public class Jep444VirtualThread {
public static void main(String[] args) throws InterruptedException {
// 创建并提交执行虚拟线程
long start = System.currentTimeMillis();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
System.out.println("time:" + (System.currentTimeMillis() - start) + "ms");
}
}
注释最后输出耗时的语句,多么丑陋的写法,Java 21 对其也有改进。
执行后发现 1.061 秒执行完毕,速度惊人。
$ java ep444VirtualThread.java
time:1061ms
设置虚拟线程名称
Thread thread1 = Thread.ofVirtual().name("v-thread").unstarted(() -> {
String threadName = Thread.currentThread().getName();
System.out.println(String.format("[%s] Hello Virtual Thread", threadName));
});
thread1.start();
输出:[v-thread] Hello Virtual Thread
启动为虚拟线程
Thread thread2 = new Thread(() -> {
String threadName = Thread.currentThread().getName();
System.out.println(String.format("[%s] Hello Virtual Thread 2", threadName));
});
Thread.startVirtualThread(thread2);
判断是否是虚拟线程
最后,可以使用 isVirtual
方法判断一个线程对象是否是虚拟线程。
Thread thread1 = Thread.ofVirtual().name("v-thread").unstarted(() -> {
String threadName = Thread.currentThread().getName();
System.out.println(String.format("[%s] Hello Virtual Thread", threadName));
});
// 判断是否是虚拟线程
System.out.println(thread1.isVirtual());
参考:https://openjdk.org/jeps/444
JEP 431. 有序集合
在 Java 中,集合类库非常重要且使用频率非常高,各种各样的集合类型有些对插入元素有序有些无序。对元素插入有序的集合如各种 List
,Deque
,以及 Linked
种类的 set
和 map
等。但是这些有序集合在 JDK 中通过类库的设计并没有体现出来。甚至使用方式也不相同。
下面是它们使用上的一些区别。
集合 | 获取第一个元素 | 获取最后一个元素 |
---|---|---|
List | list.get(0) | list.get(list.size() - 1) |
Deque | deque.getFirst() | deque.getLast() |
SortedSet | sortedSet.first() | sortedSet.last() |
LinkedHashSet | linkedHashSet.iterator().next() | // missing |
下面是在 JDK 21 之前,对几个有序集合的操作示例,可见不管是获取第一个元素、获取最后一个元素、还是逆序遍历等,操作方式都不相同,这样很容易混乱。
JDK 21 之前有序集合操作
示例:JDK 21 之前有序集合的操作
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.TreeSet;
/**
* JDK 21 之前,顺序集合中操作体验不一致
*
*/
public class JEP431Test {
public static void main(String[] args) {
// JDK 21 之前,顺序集合中操作体验不一致
List<Integer> listTemp = List.of(1, 2, 3, 4, 5);
ArrayList<Integer> list = new ArrayList(listTemp);
Deque<Integer> deque = new ArrayDeque<>(listTemp);
LinkedHashSet<Integer> linkedHashSet = new LinkedHashSet<>(listTemp);
TreeSet<Integer> sortedSet = new TreeSet<>(listTemp);
LinkedHashMap<Integer, Integer> linkedHashMap = new LinkedHashMap<>();
for (int i = 1; i <= 5; i++) {
linkedHashMap.put(i, i);
}
// 输出第一个元素
System.out.println(list.get(0));
System.out.println(deque.getFirst());
System.out.println(linkedHashSet.iterator().next());
System.out.println(sortedSet.first());
//System.out.println(linkedHashMap.firstEntry());没办法
System.out.println("-----------------------");
// 输出最后一个元素
System.out.println(list.get(list.size()-1));
System.out.println(deque.getLast());
//System.out.println(linkedHashSet()); 没办法,只能遍历
System.out.println(sortedSet.last());
//System.out.println(linkedHashMap); 没办法
System.out.println("-----------------------");
// 逆序遍历
for (var it = list.listIterator(list.size()); it.hasPrevious();) {
var e = it.previous();
System.out.print(e);
}
System.out.println();
for (var it = deque.descendingIterator(); it.hasNext();) {
var e = it.next();
System.out.print(e);
}
System.out.println();
for (Integer i : sortedSet.descendingSet()) {
System.out.print(i);
}
System.out.println();
// sortedSet linkedHashMap 逆序输出很难操作
}
}
运行输出:
$ java ./JEP431Test.java
1
1
1
1
-----------------------
5
5
5
-----------------------
54321
54321
54321
JDK 21 有序集合操作
从 JDK 21 开始,增加了 SequencedCollection
接口和 SequencedSet
接口以及SequencedMap
接口,且 在 SequencedCollection
接口定义了有序集合集中常用的操作方法。
addFirst
addLast
getFirst
getLast
removeFirst
removeLast
reversed
在 SequencedMap
接口中也增加了有序 Map 的常用操作。
firstEntry
lastEntry
pollFirstEntry
pollLastEntry
putFirst
putLast
reversed
sequencedEntrySet
sequencedKeySet
sequencedValues
下面的这个图可以很好的展示新增的有序接口的关系。
那么在 JDK 21 中,针对有序集合的操作体验就非常一致了。
// JDK 21 之后,为所有元素插入有序集合提供了一致的操作 API
List<Integer> listTemp = List.of(1, 2, 3, 4, 5);
ArrayList<Integer> list = new ArrayList(listTemp);
Deque<Integer> deque = new ArrayDeque<>(listTemp);
LinkedHashSet<Integer> linkedHashSet = new LinkedHashSet<>(listTemp);
TreeSet<Integer> sortedSet = new TreeSet<>(listTemp);
LinkedHashMap<Integer, Integer> linkedHashMap = new LinkedHashMap<>();
for (int i = 1; i <= 5; i++) {
linkedHashMap.put(i, i);
}
// 输出第一个元素
System.out.println(list.getFirst());
System.out.println(deque.getFirst());
System.out.println(linkedHashSet.getFirst());
System.out.println(sortedSet.getFirst());
System.out.println(linkedHashMap.firstEntry());
System.out.println("-----------------------");
// 输出最后一个元素
System.out.println(list.getLast());
System.out.println(list.getLast());
System.out.println(deque.getLast());
System.out.println(sortedSet.getLast());
System.out.println(linkedHashMap.lastEntry());
System.out.println("-----------------------");
// 逆序遍历
Consumer<SequencedCollection> printFn = s -> {
// reversed 逆序元素
s.reversed().forEach(System.out::print);
System.out.println();
};
printFn.accept(list);
printFn.accept(deque);
printFn.accept(linkedHashSet);
printFn.accept(sortedSet);
// 有序 map 接口是 SequencedMap,上面的 consume类型不适用
linkedHashMap.reversed().forEach((k, v) -> {
System.out.print(k);
});
输出结果:
1
1
1
1
1=1
-----------------------
5
5
5
5
5=5
-----------------------
54321
54321
54321
54321
54321
参考:https://openjdk.org/jeps/431
JEP 430. 字符串模版(预览)
Java 21 增加了字符串模版操作,类似于其他语言的字符串插值。
举例:int x = 20;int y = 3;
,如何输出 20 + 3 = 21
?
Java 21 之前
Java 21 之前写法:
// 方式1
String s = x + " + " + y + " = " + (x + y);
System.out.println(s);
// 方式2
String sb = new StringBuilder().append(x).append(" + ").append(y).append(" = ").append(x + y).toString();
System.out.println(sb);
// 方式3
String sFormat = String.format("%d + %d = %d", x, y, x + y);
System.out.println(sFormat);
输出:
20 + 3 = 23
20 + 3 = 23
20 + 3 = 23
这几种方式要么可读性不高,要么写法繁琐。
Java 21 字符串模版
Java 21 改善了这个问题,引入了字符串模版 STR
、RAW
类,在处理字符串时,可以通过 \{x}
来读取名称为 x 的变量值。这样可以方便的对字符串进行插值。
由于字符串模版是 Java 21 中的一个预览功能,因此在执行时需要添加启动参数。
$ java --enable-preview --source 21 Jep430StringTemplate.java
#注: Jep430StringTemplate.java 使用 Java SE 21 的预览功能。
Java 21 中使用字符串模版进行插值。
// JDK 21 使用字符串模版 STR 进行插值
String sTemplate = StringTemplate.STR."\{x} + \{y} = \{x+y}";
System.out.println(sTemplate);
// 字符串模版也可以先定义模版,再处理插值
StringTemplate st = StringTemplate.RAW."\{x} + \{y} = \{x+y}";
String sTemplate2 = StringTemplate.STR.process(st);
System.out.println(sTemplate2);
输出:
20 + 3 = 23
20 + 3 = 23
字符串模版适用各种类型,可以调用方法、获取属性值或者数组元素。
LocalDate now = LocalDate.now();
String nowStr = StringTemplate.STR."现在是 \{now.getYear()} 年 \{now.getMonthValue()} 月 \{now.getDayOfMonth()} 号";
System.out.println(nowStr);
// 字符串模版读取数组,字符串模版也可以嵌套
String[] infoArr = { "Hello", "Java 21", "https://www.wdbyte.com" };
String sArray = StringTemplate.STR."\{infoArr[0]}, \{STR."\{infoArr[1]}, \{infoArr[2]}"}";
System.out.println(sArray);
// 字符串模版也可以结合多行文本
String name = "https://www.wdbyte.com";
String address = "程序猿阿朗";
String json = StringTemplate.STR."""
{
"name": "\{name}",
"address": "\{address}"
}
""";
System.out.println(json);
输出:
现在是 2023 年 10 月 15 号
Hello, Java 21, https://www.wdbyte.com
{
"name": "https://www.wdbyte.com",
"address": "程序猿阿朗"
}
参考:https://openjdk.org/jeps/430
JEP 440. Record 模式
Record 模式在 Java 14 中就已经引入,在Java 16 中对 Record 可以进行 instanceof
匹配且可以进行简单结构,像下面这样:
/**
* @author https://www.wdbyte.com
* @date 2023/10/13
*/
public class Jep440Record {
public static void main(String[] args) {
Dog dog = new Dog("Husky", 1);
if (dog instanceof Dog(String name, int age)) {
String res = StringTemplate.STR."name:\{name} age:\{age}";
System.out.println(res);
}
}
}
record Dog(String name, int age) {}
输出:name:Husky age:1
而现在,可以对更复杂嵌套的 Record,进行解构。
/**
* @author https://www.wdbyte.com
* @date 2023/10/13
*/
public class Jep440Record {
public static void main(String[] args) {
Dog dog = new Dog("Husky", 1);
Object myDog = new MyDog(dog, Color.BLACK);
if (myDog instanceof MyDog(Dog(String name,int age),Color color)){
String res = StringTemplate.STR."name:\{name} age:\{age} color:\{color}";
System.out.println(res);
}
}
}
record Dog(String name, int age) {}
enum Color{WHITE,GREY,BLACK};
record MyDog(Dog dog,Color color){};
输出:name:Husky age:1 color:BLACK
参考:https://openjdk.org/jeps/440
JEP 441. switch模式匹配
在 Java 21 中,switch 可以用于匹配类型了。
/**
* @author https://www.wdbyte.com
*/
public class Jep441SwitchPatten {
public static void main(String[] args) {
String r1 = formatterPatternSwitch(Integer.valueOf(1));
String r2 = formatterPatternSwitch(new String("www.wdbyte.com"));
String r3 = formatterPatternSwitch(Double.valueOf(3.14D));
System.out.println(r1);
System.out.println(r2);
System.out.println(r3);
}
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();
};
}
}
输出:
int 1
String www.wdbyte.com
double 3.140000
Java 21 中 switch 还可以用于 null
判断。
static void testFooBarNew(String s) {
switch (s) {
case null -> System.out.println("Oops");
case "Foo", "Bar" -> System.out.println("Great");
default -> System.out.println("Ok");
}
}
参考:https://openjdk.org/jeps/441
JEP 445. 未命名的类和main方法(预览)
我们都知道一个输出 Hello, World!
字符串的Java 程序应该怎么写。
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
看起来是一个非常简单的例子,但是对于 Java 初学者来说,可能有些知识他并不了解。
如:
- 代码中的
public
权限修饰。 - 静态方法
static
的含义。 String[]
数组的定义。
这无形中提高的初学者的入门门槛,且对教学过程也不友好,不这是一个渐进式的学习过程。基于如下的几个原因,Java 需要提供一些简化写法。
- 为 Java 提供一个平稳的入门通道,以便教育工作者可以循序渐进地介绍编程概念。
- 帮助学生以简洁的方式编写基本程序,并随着他们的技能增长而优雅地扩展他们的代码。
- 减少编写简单程序(例如脚本和命令行实用程序)的仪式。
- 不要引入单独的 Java 初学者方言。
- 不要引入单独的初学者工具链;学生程序应该使用与编译和运行任何 Java 程序相同的工具来编译和运行。
在 Java 21 中,你可以只写一个 main
函数就可以运行程序。
void main() {
System.out.println("Hello, Java 21!");
}
执行输出:
$ cat Jep445HelloJava.java
void main(){
System.out.println("Hello, Java 21!");
}
$ java --enable-preview --source 21 Jep445HelloJava.java
注: Jep445HelloJava.java 使用 Java SE 21 的预览功能。
注: 有关详细信息,请使用 -Xlint:preview 重新编译。
Hello, Java 21!
参考:https://openjdk.org/jeps/445
JEP 443. 未命名模式和变量(预览)
简单是的说,如果一个变量可能用不到,那么可以使用 _
代替。
假设有这么一段逻辑:
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8);
int count = 0;
for (Integer i : list) {
count++;
}
System.out.println(count);
这里的临时变量 i
没有使用到,那么可以使用 _
代替。
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8);
int count = 0;
for (Integer _ : list) {
count++;
}
System.out.println(count);
甚至可以定义多个对象,都无需使用,都可以使用 _
命名。
String _ = "123213";
Integer _ = 123;
运行:
$ cat Jep443Unname.java
import java.util.List;
/**
* @author https://www.wdbyte.com
* @date 2023/10/15
*/
public class Jep443Unname {
public static void main(String[] args) {
String _ = "123213";
Integer _ = 123;
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8);
int count = 0;
for (Integer _ : list) {
count++;
}
System.out.println(count);
}
}
$ java --enable-preview --source 21 Jep443Unname.java
注: Jep443Unname.java 使用 Java SE 21 的预览功能。
注: 有关详细信息,请使用 -Xlint:preview 重新编译。
8
参考:https://openjdk.org/jeps/443
其他更新
JEP 442. 外部函数和内存 API(三次预览)
此功能引入的 API 允许 Java 开发者与 JVM 之外的代码和数据进行交互,通过调用外部函数(JVM 之外)和安全的访问外部内存(非 JVM 管理),让 Java 程序可以调用本机库并处理本机数据,而不会像 JNI 一样存在很多安全风险。
这不是一个新功能,自 Java 14 就已经引入,此次对其进行了性能、通用性、安全性、易用性上的优化。
历史
- Java 14 JEP 370 引入了外部内存访问 API(孵化器)。
- Java 15 JEP 383 引入了外部内存访问 API(第二孵化器)。
- Java 16 JEP 389 引入了外部链接器 API(孵化器)。
- Java 16 JEP 393 引入了外部内存访问 API(第三孵化器)。
- Java 17 JEP 412 引入了外部函数和内存 API(孵化器)。
- Java 18 JEP 419 引入了外部函数和内存 API(二次孵化器)。
- Java 19 JEP 424 引入了外部函数和内存 API(孵化器)。
- Java 20 JEP 434 引入了外部函数和内存 API(二次预览)
JEP 448. Vector API(六次孵化)
再次提高性能,实现优于等效标量计算的性能。这是通过引入一个 API 来表达矢量计算,该 API 在运行时可靠地编译为支持的 CPU 架构上的最佳矢量指令,从而实现优于等效标量计算的性能。Vector API 在 JDK 16 到 19 中孵化。JDK 20 整合了这些版本用户的反馈以及性能改进和实现增强。Java 21 继续改进。
JEP 446. 作用域值
引入作用域值,这些值可以安全有效地共享给方法,而无需使用方法参数。它们优于线程局部变量,特别是在使用大量虚拟线程时。这是一个预览 API。
JEP 449. 弃用 windows 32 位 x86 端口
因为 Windows 10 是最后一个支持 32 位操作的 Windows 操作系统,将于2025 年 10 月终止生命周期。且虚拟线程在 x86-32 上无法实现,所以会弃用相关端口,未来会在相关文档中删除。
JEP453. Structured Concurrency (预览)
通过引入结构化并发 API 来简化并发编程。结构化并发将在不同线程中运行的相关任务组视为单个工作单元,从而简化错误处理和取消、提高可靠性并增强可观察性。这是一个预览 API。
-
相关 Java 19,JEP 428:结构化并发(孵化)
-
相关 Java 20,JEP 437: Structured Concurrency(二次孵化)
JEP 439. ZGC
通过扩展 ZGC 以维护年轻对象和老对象的独立代,来提高应用程序的性能。这将使 ZGC 能够更频繁地回收年轻对象,因为它们往往会早早死亡。
JEP 451. 准备禁止动态加载代理程序
当代理程序动态加载到正在运行的 JVM 中时,发出警告。这些警告旨在为将来的发布做准备,该发布默认情况下禁止动态加载代理程序,以默认方式提高完整性。在任何发布中,加载代理程序的可维护性工具不会导致发出警告。
JEP 452. Key Encapsulation Mechanism API
一种针对密钥封装机制(KEMs)的API,这是一种使用公钥密码学来保护对称密钥的加密技术。
一如既往,文章中代码存放在 Github.com/niumoo/javaNotes.