前不久 JDK 20 / Java 20 正式发布了,下面一起来看看,都有哪些新特性

这是一个短期维护版本,将获得六个月的支持。尽管如此,但它仍然可用于生产环境中。

根据开发计划,下一个 LTS 版本就是将于 2023 年 9 月发布的 JDK 21。

JDK 20只有7 个新特性,今天早上抽时间看了一下新特性,并对这些新特性做了简单的解读,希望对你有帮助!

  • JEP 429:作用域值(第一次孵化)
  • JEP 432:记录模式(第二次预览)
  • JEP 433:switch 模式匹配(第四次预览)
  • JEP 434: 外部函数和内存 API(第二次预览)
  • JEP 436: 虚拟线程(第二次预览)
  • JEP 437: 结构化并发(第二次孵化)
  • JEP 432:向量 API(第五次孵化)

JEP 429:作用域值(第一次孵化)

作用域值(Scoped Values)它可以在线程内和线程间共享不可变的数据,优于线程局部变量,尤其是在使用大量虚拟线程时。

1
2
3
4
5
6
7
8
final static ScopedValue<...> V = new ScopedValue<>();

// In some method
ScopedValue.where(V, <value>)
.run(() -> { ... V.get() ... call methods ... });

// In a method called directly or indirectly from the lambda expression
... V.get() ...

作用域值允许在大型程序中的组件之间安全有效地共享数据,而无需求助于方法参数。

JEP 432:记录模式(第二次预览)

记录模式(Record Patterns) 可对 record 的值进行解构,可以嵌套记录模式和类型模式,实现强大的、声明性的和可组合的数据导航和处理形式。

记录模式不能单独使用,而是要与 instanceof 或 switch 模式匹配一同使用。

记录模式在Java 19进行了第一次预览, 由JEP 405提出。JDK 20 中是第二次预览,由JEP 432提出。这次的改进包括:

  • 添加对通用记录模式类型参数推断的支持,
  • 添加对记录模式的支持以出现在增强语句的标题中for
  • 删除对命名记录模式的支持。

JEP 433:switch 模式匹配(第四次预览)

正如instanceof一样,switch也紧跟着增加了类型匹配自动转换功能。

instanceof代码示例:

1
2
3
4
5
6
7
8
9
10
// Old code
if (o instanceof String) {
String s = (String)o;
... use s ...
}

// New code
if (o instanceof String s) {
... use s ...
}

switch代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Old code
static String formatter(Object o) {
String formatted = "unknown";
if (o instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (o instanceof Long l) {
formatted = String.format("long %d", l);
} else if (o instanceof Double d) {
formatted = String.format("double %f", d);
} else if (o instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}

// New code
static String formatterPatternSwitch(Object o) {
return switch (o) {
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 -> o.toString();
};
}

switch模式匹配分别在 Java17、Java18、Java19 中进行了预览,Java20 是第四次预览了。每一次的预览基本都会有一些小改进,这里就不细提了。

JEP 434: 外部函数和内存 API(第二次预览)

Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。

外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由JEP 412提出。Java 18 中进行了第二次孵化,由JEP 419提出。Java 19 中是第一次预览,由JEP 424提出。

JDK 20 中是第二次预览,由JEP 434提出,这次的改进包括:

  • MemorySegment 和 MemoryAddress 抽象的统一
  • 增强的 MemoryLayout 层次结构
  • MemorySession拆分为Arena和SegmentScope,以促进跨维护边界的段共享。

在没有外部函数和内存 API 之前:

  • Java 通过 sun.misc.Unsafe 提供一些执行低级别、不安全操作的方法(如直接访问系统内存资源、自主管理内存资源等),Unsafe 类让 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力的同时,也增加了 Java 语言的不安全性,不正确使用 Unsafe 类会使得程序出错的概率变大。
  • Java 1.1 就已通过 Java 原生接口(JNI)支持了原生方法调用,但并不好用。JNI 实现起来过于复杂,步骤繁琐(具体的步骤可以参考这篇文章:Guide to JNI (Java Native Interface)),不受 JVM 的语言安全机制控制,影响 Java 语言的跨平台特性。并且,JNI 的性能也不行,因为 JNI 方法调用不能从许多常见的 JIT 优化(如内联)中受益。虽然JNA、JNR和JavaCPP等框架对 JNI 进行了改进,但效果还是不太理想。

引入外部函数和内存 API 就是为了解决 Java 访问外部函数和外部内存存在的一些痛点。

Foreign Function & Memory API (FFM API) 定义了类和接口:

  • 分配外部内存 :MemorySegment、MemoryAddress和SegmentAllocator);
  • 操作和访问结构化的外部内存:MemoryLayout, VarHandle
  • 控制外部内存的分配和释放:MemorySession
  • 调用外部函数:Linker、FunctionDescriptor和SymbolLookup

下面是 FFM API 使用示例,这段代码获取了 C 库函数的radixsort方法句柄,然后使用它对 Java 数组中的四个字符串进行排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1. 在C库路径上查找外部函数
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
MethodHandle radixSort = linker.downcallHandle(
stdlib.lookup("radixsort"), ...);
// 2. 分配堆上内存以存储四个字符串
String[] javaStrings = { "mouse", "cat", "dog", "car" };
// 3. 分配堆外内存以存储四个指针
SegmentAllocator allocator = implicitAllocator();
MemorySegment offHeap = allocator.allocateArray(ValueLayout.ADDRESS, javaStrings.length);
// 4. 将字符串从堆上复制到堆外
for (int i = 0; i < javaStrings.length; i++) {
// 在堆外分配一个字符串,然后存储指向它的指针
MemorySegment cString = allocator.allocateUtf8String(javaStrings[i]);
offHeap.setAtIndex(ValueLayout.ADDRESS, i, cString);
}
// 5. 通过调用外部函数对堆外数据进行排序
radixSort.invoke(offHeap, javaStrings.length, MemoryAddress.NULL, '\0');
// 6. 将(重新排序的)字符串从堆外复制到堆上
for (int i = 0; i < javaStrings.length; i++) {
MemoryAddress cStringPtr = offHeap.getAtIndex(ValueLayout.ADDRESS, i);
javaStrings[i] = cStringPtr.getUtf8String(0);
}
assert Arrays.equals(javaStrings, new String[] {"car", "cat", "dog", "mouse"}); // true

JEP 436: 虚拟线程(第二次预览)

虚拟线程(Virtual Thread-)是 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP),许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量。

虚拟线程在其他多线程语言中已经被证实是十分有用的,比如 Go 中的 Goroutine、Erlang 中的进程。

虚拟线程避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂,可以有效减少编写、维护和观察高吞吐量并发应用程序的工作量。

知乎有一个关于 Java 19 虚拟线程的讨论,感兴趣的可以去看看:https://www.zhihu.com/question/536743167

Java 虚拟线程的详细解读和原理可以看下面这两篇文章:

虚拟线程在 Java 19 中进行了第一次预览,由JEP 425提出。JDK 20 中是第二次预览,做了一些细微变化,这里就不细提了。

JEP 437: 结构化并发(第二次孵化)

Java 19 引入了结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代java.util.concurrent,目前处于孵化器阶段。

结构化并发将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。也就是说,结构化并发保留了单线程代码的可读性、可维护性和可观察性。

结构化并发的基本 API 是StructuredTaskScope。StructuredTaskScope支持将任务拆分为多个并发子任务,在它们自己的线程中执行,并且子任务必须在主任务继续之前完成。

StructuredTaskScope的基本用法如下:

1
2
3
4
5
6
7
8
9
try (var scope = new StructuredTaskScope<Object>()) {
// 使用fork方法派生线程来执行子任务
Future<Integer> future1 = scope.fork(task1);
Future<String> future2 = scope.fork(task2);
// 等待线程完成
scope.join();
// 结果的处理可能包括处理或重新抛出异常
... process results/exceptions ...
} // close

结构化并发非常适合虚拟线程,虚拟线程是 JDK 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。

JDK 20 中对结构化并发唯一变化是更新为支持在任务范围内创建的线程StructuredTaskScope继承范围值 这简化了跨线程共享不可变数据,详见JEP 429

JEP 432:向量 API(第五次孵化)

向量(Vector) API 最初由JEP 338提出,并作为孵化 API集成到 Java 16 中。第二轮孵化由JEP 414提出并集成到 Java 17 中,第三轮孵化由JEP 417提出并集成到 Java 18 中,第四轮由JEP 426提出并集成到了 Java 19 中。

Java20 的这次孵化基本没有改变向量 API ,只是进行了一些错误修复和性能增强,详见JEP 438

向量计算由对向量的一系列操作组成。向量 API 用来表达向量计算,该计算可以在运行时可靠地编译为支持的 CPU 架构上的最佳向量指令,从而实现优于等效标量计算的性能。

向量 API 的目标是为用户提供简洁易用且与平台无关的表达范围广泛的向量计算。

这是对数组元素的简单标量计算:

1
2
3
4
5
void scalarComputation(float[] a, float[] b, float[] c) {
for (int i = 0; i < a.length; i++) {
c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
}
}

这是使用Vector API进行的等效向量计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;

void vectorComputation(float[] a, float[] b, float[] c) {
int i = 0;
int upperBound = SPECIES.loopBound(a.length);
for (; i < upperBound; i += SPECIES.length()) {
// FloatVector va, vb, vc;
var va = FloatVector.fromArray(SPECIES, a, i);
var vb = FloatVector.fromArray(SPECIES, b, i);
var vc = va.mul(va)
.add(vb.mul(vb))
.neg();
vc.intoArray(c, i);
}
for (; i < a.length; i++) {
c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
}
}

下载体验:https://jdk.java.net/20/