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 中,集合类库非常重要且使用频率非常高,各种各样的集合类型有些对插入元素有序有些无序。对元素插入有序的集合如各种 ListDeque,以及 Linked 种类的 setmap 等。但是这些有序集合在 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

下面的这个图可以很好的展示新增的有序接口的关系。

img

那么在 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 改善了这个问题,引入了字符串模版 STRRAW 类,在处理字符串时,可以通过 \{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 初学者来说,可能有些知识他并不了解。

如:

  1. 代码中的 public 权限修饰。
  2. 静态方法 static 的含义。
  3. 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.