Java19新特性介绍以及简单体验

Java19新特性介绍简单使用

Java19于 2022年9 月 20 日正式发布以供生产使用,非长期支持版本。不过,JDK 19 中有一些比较重要的新特性值得关注。

其中 外部函数API、虚拟线程和结构化并发这 3个感觉属于比较重要的新特性,接下来对这3个特性进行简单的介绍并简单的上手代码体验

JEP 424: 外部函数和内存 API(预览)

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

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

在没有外部函数和内存 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 优化(如内联)中受益。虽然JNAJNRJavaCPP等框架对 JNI 进行了改进,但效果还是不太理想。

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 在C库路径上查找外部函数
SymbolLookup stdlib = Linker.nativeLinker().defaultLookup();

// 2. 获取 C 标准库中“strlen”函数的句柄
MethodHandle strlen = Linker.nativeLinker().downcallHandle(
stdlib.lookup("strlen").orElseThrow(),
FunctionDescriptor.of(JAVA_LONG, ADDRESS));

// 3. 将 Java 字符串转换为 C 字符串并存储在堆外内存中
MemorySegment str = implicitAllocator().allocateUtf8String("hello Java19!");

// 4. 调用外部函数
long len = (long) strlen.invoke(str);

System.out.println("len = " + len);

JEP 425: 虚拟线程(预览)

虚拟线程(Virtual Thread-)是 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP),许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量。虚拟线程在其他多线程语言中已经被证实是十分有用的,比如 Go 中的 Goroutine、Erlang 中的进程。虚拟线程避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂,可以有效减少编写、维护和观察高吞吐量并发应用程序的工作量。虚拟线程应该是JDK19最重磅的特性,首次集成进来即为preview的API虚拟线程是由JDK提供的用户态线程,类似golang的goroutine,erlang的processes虚拟线程采用的是M:N的调度模式,即M数量的虚拟线程运行在N个Thread上,使用的是ForkJoinPool以FIFO模式来进行调度,N默认是为Runtime.availableProcessors(),可以通过jdk.virtualThreadScheduler.parallelism来修改

1
2
3
4
5
6
7
8
9
10
11
12
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10000).forEach(i -> {
executor.execute(() -> {
try {
Thread.sleep(Duration.ofSeconds(1));
System.out.println(i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
});
}

如上使用了少数几个OS线程来运行1w个虚拟线程,虚拟线程在超过上千个非CPU密集并发任务场景可以显著提升系统的吞吐率,像某些网络IO请求虽然是block的代码,但是因为引入的是虚拟线程,系统可以很好地伸缩;当虚拟线程block在IO或者其他操作(BlockingQueue.take())时,虚拟线程会从Thread unmount,当操作完成才重新mount上继续执行。不过有些操作不会unmount虚拟线程,会一同thread和底层的OS线程一起block住(比如进入synchronized代码块/方法,比如执行一个native方法或者foreign function)。 虚拟线程开销不大,因而不需要使用池化技术

JEP 428: 结构化并发(孵化)

JDK 19 引入了结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代java.util.concurrent,目前处于孵化器阶段。结构化并发将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。也就是说,结构化并发保留了单线程代码的可读性、可维护性和可观察性。结构化并发的基本 API 是StructuredTaskScopeStructuredTaskScope 支持将任务拆分为多个并发子任务,在它们自己的线程中执行,并且子任务必须在主任务继续之前完成。

StructuredTaskScope 的基本用法如下:

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
26
27
28
29
30
31
32
   			//java17的随机数生成器
RandomGeneratorFactory<RandomGenerator> l128X256MixRandom = RandomGeneratorFactory.of("L128X256MixRandom");
RandomGenerator randomGenerator = l128X256MixRandom.create(System.currentTimeMillis());
//结构化并发
try (var scope = new StructuredTaskScope<>()) {
// 使用fork方法派生线程来执行子任务
Future<Integer> task1 = scope.fork(() -> {
int i = randomGenerator.nextInt(10);
Thread.sleep(i);
System.out.println("task1 complete....");
return i;
});
Future<Integer> task2 = scope.fork(() -> {
int i = randomGenerator.nextInt(5);
Thread.sleep(i);
System.out.println("task2 complete....");
return i;
});
// 等待线程完成
scope.join();
System.out.println("task complete....");
System.out.println("task1 return " + task1.get());
System.out.println("task2 return " + task2.get());
} catch (Exception e) {
e.printStackTrace();
}
---输出结果---
task2 complete....
task1 complete....
task complete....
task1 return 8
task2 return 3

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