你好,我是看山。
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。让我们跟随 Java 的脚步,配合示例讲解,看一看每个版本的新特性,本期是 Java20 的新特性。
概述
Java20 在 2023 年 3 月 21 日发布GA版本,共七大特性,这些新特性处于预览或孵化阶段,旨在简化并发编程、提高性能和增强语言表达能力。这七大特性分别是:
- JEP 429: 作用域值(Scoped Values,孵化),来自Amber项目,允许在线程内和线程间共享不可变数据,优于传统的线程局部变量。它主要用于在多线程应用中安全地传递和访问数据,例如从父线程向子线程传递数据。
- JEP 432: Record模式(Record Patterns,第二次预览),来自Amber项目,用于简化数据结构的模式匹配。
- JEP 433: Switch模式匹配(Pattern Matching for switch,第四次预览),来自Amber项目,用于改进switch语句中的模式匹配。
- JEP 434: 外部函数和内存API(Foreign Function & Memory API,FFM API,第二次预览),来自Loom项目,旨在提供更高效的内存管理和外部函数调用能力。
- JEP 436: 虚拟线程(Virtual Threads,第二次预览),来自Loom项目,通过引入轻量级虚拟线程来简化高吞吐量并发应用程序的开发和维护。
- JEP 437: 结构化并发API (Structured Concurrency,第二次孵化),来自Loom项目,旨在简化多线程编程,提高应用程序的可靠性和可观察性。
- JEP 438: 向量API(Vector API,第五次孵化),用于表达向量计算,提高了性能和代码可读性。
接下来我们一起看看这些特性。
预览功能
JEP 432: Record模式(第二次预览)
Record类型提供不可变对象的简单实现(其实就是Java Bean,但是省略一堆的getter、setter、hashcode、equals、toString等方法),Java16开始一直在演化增强(参见从小工到专家的 Java 进阶之旅)。
Record模式归属于Amber项目的一部分,Amber项目旨在通过小而美的方式,增强Java语言特性。本次的Record模式,主要是使Record类型可以直接在instanceof和switch模式匹配中使用。
Record模式最初作为预览功能在JEP 405中提出,并在Java19中交付。JEP 432提出了第二次预览,基于持续的经验和反馈进行了进一步的完善。主要变化包括:
- 支持推断泛型记录模式的类型参数,
- 允许记录模式出现在增强for循环的头部,
- 移除了对命名记录模式的支持。
我们一起看个示例,比如有下面几个基础元素:
// 颜色
enum Color { RED, GREEN, BLUE}
// 点
record Point(int x, int y) {}
// 带颜色的点
record ColoredPoint(Point p, Color color) {}
// 正方形
record Square(ColoredPoint upperLeft, ColoredPoint lowerRight) {}
我们分别通过instanceof模式匹配和switch模式匹配判断输入参数的类型,打印不同的格式:
private static void instancePatternsAndPrint(Object o) {
if (o instanceof Square(ColoredPoint upperLeft, ColoredPoint lowerRight)) {
System.out.println("Square类型:" + upperLeft + " " + lowerRight);
} else if (o instanceof ColoredPoint(Point(int x, int y), Color color)) {
System.out.println("ColoredPoint类型:" + x + " " + y + " " + color);
} else if (o instanceof Point p) {
System.out.println("Point类型:" + p);
}
}
private static void switchPatternsAndPrint(Object o) {
switch (o) {
case Square(ColoredPoint upperLeft, ColoredPoint lowerRight) -> {
System.out.println("Square类型:" + upperLeft + " " + lowerRight);
}
case ColoredPoint(Point(int x, int y), Color color) -> {
System.out.println("ColoredPoint类型:" + x + " " + y + " " + color);
}
case Point p -> {
System.out.println("Point类型:" + p);
}
default -> throw new IllegalStateException("Unexpected value: " + o);
}
}
我们通过main方法执行下:
public static void main(String[] args) {
var p = new Point(1, 2);
var cp1 = new ColoredPoint(p, Color.RED);
var cp2 = new ColoredPoint(p, Color.GREEN);
var square = new Square(cp1, cp2);
instancePatternsAndPrint(square);
instancePatternsAndPrint(cp1);
instancePatternsAndPrint(p);
switchPatternsAndPrint(square);
switchPatternsAndPrint(cp1);
switchPatternsAndPrint(p);
}
// 结果是:
//
// Square类型:ColoredPoint[p=Point[x=1, y=2], color=RED] ColoredPoint[p=Point[x=1, y=2], color=GREEN]
// ColoredPoint类型:1 2 RED
// Point类型:Point[x=1, y=2]
//
// Square类型:ColoredPoint[p=Point[x=1, y=2], color=RED] ColoredPoint[p=Point[x=1, y=2], color=GREEN]
// ColoredPoint类型:1 2 RED
// Point类型:Point[x=1, y=2]
JEP 433: switch模式匹配(第四次预览)
在Java20之前,switch模式匹配的选择器表达式只能是基本数据类型或字符串。在Java20中,这一限制被取消,允许选择器表达式为任何引用类型,包括instanceof操作符。这意味着可以更灵活地使用对象、数组、列表等复杂数据结构作为switch语句的基础,从而简化代码并提高可读性。
switch模式匹配允许在switch语句中使用模式来测试表达式,每个模式都有特定的动作,从而可以简洁、安全地表达复杂的数据导向查询。
主要功能包括:
- 增强表达性和适用性:允许在case标签中出现模式,扩展了switch表达式和语句的表达能力和适用范围。
- 放宽对null的敌意:当需要时,可以放宽switch对null的敌意。
- 提高安全性:要求模式switch语句覆盖所有可能的输入值,增加了switch语句的安全性。
- 保持兼容性:确保所有现有的switch表达式和语句继续无变化地编译并以相同的语义执行。
通过代码看下switch模式匹配的魅力;
static String formatValue(Object obj) {
return switch (obj) {
case null -> "null";
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);
case Person(String name, String address) -> String.format("Person %s %s", name, address);
default -> obj.toString();
};
}
public record Person(String name, String address) {}
public static void main(String[] args) {
System.out.println(formatValue(10));
System.out.println(formatValue(20L));
System.out.println(formatValue(3.14));
System.out.println(formatValue("Hello"));
System.out.println(formatValue(null));
System.out.println(formatValue(new Person("Howard", "Beijing")));
}
// 运行结果
// int 10
// long 20
// double 3.140000
// String Hello
// null
// Person Howard Beijing
JEP 434: 外部函数和内存API(第二次预览)
外部函数和内存API(Foreign Function & Memory API,简称FFM API)旨在提供一种机制,使Java程序能够与Java运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即JVM之外的代码)以及安全地访问外部内存(即非JVM管理的内存),该API使得Java程序能够调用本地库并处理本地数据,而无需使用JNI带来的脆弱性和危险。
简单说,FFM API是为了替换JNI的函数,在Java20中是第二次预览版,最终会在Java22中转正。
FFM API的主要功能包括:
- 易用性:用一个更优越、完全Java开发模型替代了Java原生接口(JNI)。
- 性能:提供与现有API(如JNI和sun.misc.Unsafe )相当甚至更好的性能。
- 通用性:支持操作不同类型的外部内存(例如,本地内存、持久内存和托管堆内存),并计划随着时间的推移适应其他平台(例如,32位x86)和用C语言之外的语言(例如C++、Fortran)编写的外部函数。
- 安全性:允许程序在默认情况下对外部内存执行不安全的操作,但会向用户发出警告。
我们来看一个官方给的示例:如何获取C库函数radixsort
的方法句柄,并使用它来对四个字符串进行排序。
// 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 = SegmentAllocator.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
友情提示:官方的代码仅是示意,很多API还在调整,只有等到正式发行时才能暂时确定下来。
JEP 436: 虚拟线程(第二次预览)
虚拟线程是一种轻量级的线程实现,旨在显著降低编写、维护和观察高吞吐量并发应用程序的难度。它们占用的资源少,不需要被池化,可以创建大量虚拟线程,特别适用于IO密集型任务,因为它们可以高效地调度大量虚拟线程来处理并发请求,从而显著提高程序的吞吐量和响应速度。
相比于传统现成,虚拟线程性能显著提升。
虚拟线程在Java19中第一次预览,在Java20第二次预览,本次预览没有引入新的API,做了一些最终化处理,为Java21正式发布做准备。
虚拟线程有下面几个特点:
- 轻量级:虚拟线程是JVM内部实现的轻量级线程,不需要操作系统内核参与,创建和上下文切换的成本远低于传统的操作系统线程(即平台线程),且占用的内存资源较少。
- 减少CPU时间消耗:由于虚拟线程不依赖于操作系统平台线程,因此在进行线程切换时耗费的CPU时间会大大减少,从而提高了程序的执行效率。
- 简化多线程编程:虚拟线程通过结构化并发API来简化多线程编程,使得开发者可以更容易地编写、维护和观察高吞吐量并发应用程序。
- 适用于大量任务场景:虚拟线程非常适合需要创建和销毁大量线程的任务、需要执行大量计算的任务(如数据处理、科学计算等)以及需要实现任务并行执行以提高程序性能的场景。
- 提高系统吞吐量:通过对虚拟线程的介绍和与Go协程的对比,可以看出虚拟线程能够大幅提高系统的整体吞吐量。
虽然虚拟线程是预览功能,不妨碍我们一起试用下。
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
System.out.println(Thread.currentThread().getName() + ": " + i);
return i;
});
});
}
Thread.startVirtualThread(() -> {
System.out.println("Hello from a virtual thread[Thread.startVirtualThread]");
});
final ThreadFactory factory = Thread.ofVirtual().factory();
factory.newThread(() -> {
System.out.println("Hello from a virtual thread[ThreadFactory.newThread]");
})
.start();
虚拟线程为了降低使用门槛,直接提供了与原生线程类似的方法:
Executors.newVirtualThreadPerTaskExecutor()
,可以像普通线程池一样创建虚拟线程。Thread.startVirtualThread
,通过工具方法直接创建并运行虚拟线程。Thread.ofVirtual().factory().newThread()
,另一个工具方法可以创建并运行虚拟线程。Thread
还有一个ofPlatform()
方法,用来构建普通线程。
通过本地简单测试(在「公众号:看山的小屋」回复”java”获取源码),1w个模拟线程运行时,性能方面虚拟线程 > 线程池。
孵化功能
JEP 429: 作用域值(孵化)
JEP 429: 作用域值(Scoped Values),旨在促进在线程内和线程间共享不可变数据。这一特性为现代 Java 应用程序提供了一种更高效和安全的替代传统 ThreadLocal 机制的方法,尤其是在并发编程的背景下。
作用域值允许开发者定义一个变量,该变量可以在特定的作用域内访问,包括当前线程及其创建的任何子线程。这一机制特别适用于在方法调用链中隐式传递数据,而无需在方法签名中添加额外的参数。
作用域值的主要特点:
- 不可变性:作用域值是不可变的,这意味着一旦设置,其值就不能更改。这种不可变性减少了并发编程中意外副作用的风险。
- 作用域生命周期:作用域值的生命周期仅限于 run 方法定义的作用域。一旦执行离开该作用域,作用域值将不再可访问。
- 继承性:子线程会自动继承父线程的作用域值,从而允许在线程边界间无缝共享数据。
在之前,在多线程间传递数据,我们会使用ThreadLocal
来保存当前线程变量,用完需要手动清理,如果忘记清理或者使用不规范,可能导致内存泄漏等问题。作用域值通过自动管理生命周期和内存,减少了这种风险。
我们一起看下作用域值的使用:
// 声明一个作用域值用于存储用户名
public final static ScopedValue<String> USERNAME = ScopedValue.newInstance();
private static final Runnable printUsername = () ->
System.out.println(Thread.currentThread().threadId() + " 用户名是 " + USERNAME.get());
public static void main(String[] args) throws Exception {
// 将用户名 "Bob" 绑定到作用域并执行 Runnable
ScopedValue.where(USERNAME, "Bob").run(() -> {
printUsername.run();
new Thread(printUsername).start();
});
// 将用户名 "Chris" 绑定到另一个作用域并执行 Runnable
ScopedValue.where(USERNAME, "Chris").run(() -> {
printUsername.run();
new Thread(() -> {
new Thread(printUsername).start();
printUsername.run();
}).start();
});
// 检查在任何作用域外 USERNAME 是否被绑定
System.out.println("用户名是否被绑定: " + USERNAME.isBound());
}
写起来干净利索,而且功能更强。
JEP 437: 结构化并发API (第二次孵化)
JEP 437: 结构化并发API(Structured Concurrency API)旨在简化多线程编程,通过引入一个API来处理在不同线程中运行的多个任务作为一个单一工作单元,从而简化错误处理和取消操作,提高可靠性,并增强可观测性。这是一个处于孵化阶段的API。
结构化并发API提供了明确的语法结构来定义子任务的生命周期,并启用一个运行时表示线程间的层次结构。这有助于实现错误传播和取消以及并发程序的有意义观察。
Java使用异常处理机制来管理运行时错误和其他异常。当异常在代码中产生时,如何被传递和处理的过程称为异常传播。
在结构化并发环境中,异常可以通过显式地从当前环境中抛出并传播到更大的环境中去处理。
在Java并发编程中,非受检异常的处理是程序健壮性的重要组成部分。特别是对于非受检异常的处理,这关系到程序在遇到错误时是否能够优雅地继续运行或者至少提供有意义的反馈。
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var task1 = scope.fork(() -> {
Thread.sleep(1000);
return "Result from task 1";
});
var task2 = scope.fork(() -> {
Thread.sleep(2000);
return "Result from task 2";
});
scope.join();
scope.throwIfFailed(RuntimeException::new);
System.out.println(task1.get());
System.out.println(task2.get());
} catch (Exception e) {
e.printStackTrace();
}
在这个例子中,handle()方法使用StructuredTaskScope来并行执行两个子任务:task1和task2。通过使用try-with-resources语句自动管理资源,并确保所有子任务都在try块结束时正确完成或被取消。这种方式使得线程的生命周期和任务的逻辑结构紧密相关,提高了代码的清晰度和错误处理的效率。使用 StructuredTaskScope 可以确保一些有价值的属性:
- 错误处理与短路:如果task1或task2子任务中的任何一个失败,另一个如果尚未完成则会被取消。(这由 ShutdownOnFailure 实现的关闭策略来管理;还有其他策略可能)。
- 取消传播:如果在运行上面方法的线程在调用 join() 之前或之中被中断,则线程在退出作用域时会自动取消两个子任务。
- 清晰性:设置子任务,等待它们完成或被取消,然后决定是成功(并处理已经完成的子任务的结果)还是失败(子任务已经完成,因此没有更多需要清理的)。
- 可观察性:线程转储清楚地显示了任务层次结构,其中运行task1或task2的线程被显示为作用域的子任务。
上面的示例能够很好的解决我们的一个痛点,有两个可并行的任务A和B,A+B才是完整结果,任何一个失败,另外一个也不需要成功,结构化并发API就可以很容易的实现这个逻辑。
JEP 438: 向量API(第五次孵化)
向量API的功能是提供一个表达向量计算的API,这些计算在运行时可靠地编译成支持的CPU架构上的最优向量指令,从而实现比等效标量计算更优的性能。
下面这个是官方给的示例:
// 标量计算示例
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;
}
}
// 使用向量API的向量计算示例
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;
}
}
向量API在Java中的独特优势在于其高效的并行计算能力、丰富的向量化指令集、跨平台的数据并行算法支持以及对机器学习的特别优化。
文末总结
本文介绍了 Java20 新增的特性,完整的特性清单可以从 https://openjdk.org/projects/jdk/20/ 查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
青山不改,绿水长流,我们下次见。
推荐阅读
- 从小工到专家的 Java 进阶之旅
- 一文掌握 Java8 Stream 中 Collectors 的 24 个操作
- 一文掌握 Java8 的 Optional 的 6 种操作
- 使用 Lambda 表达式实现超强的排序功能
- Java8 的时间库(1):介绍 Java8 中的时间类及常用 API
- Java8 的时间库(2):Date 与 LocalDate 或 LocalDateTime 互相转换
- Java8 的时间库(3):开始使用 Java8 中的时间类
- Java8 的时间库(4):检查日期字符串是否合法
- Java8 的新特性
- Java9 的新特性
- Java10 的新特性
- Java11 中基于嵌套关系的访问控制优化
- Java11 的新特性
- Java12 的新特性
- Java13 的新特性
- Java14 的新特性
- Java15 的新特性
- Java16 的新特性
- Java17 的新特性
- Java18 的新特性
- Java19 的新特性
- Java20 的新特性
- Java21 的新特性
- Java22 的新特性
- Java23 的新特性
- Java24 的新特性
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java20 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java20 的新特性