Java22 的新特性

你好,我是看山。

本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。

从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。让我们跟随 Java 的脚步,配合示例讲解,看一看每个版本的新特性,本期是 Java22 的新特性。

概述

Java22 在 2024 年 3 月 19 日发布GA版本,共十二大特性:

  • JEP 423: G1垃圾回收器的区域固定(Region Pinning for G1)
  • JEP 447: super(…)前置语句(Statements before super(…),预览)
  • JEP 454: 外部函数和内存API(Foreign Function & Memory API)
  • JEP 456: 未命名变量和模式(Unnamed Variables & Patterns)
  • JEP 457: 类文件API(Class-File API,预览)
  • JEP 458: 启动多文件源代码程序(Launch Multi-File Source-Code Programs)
  • JEP 459: 字符串模板(String Templates,第二次预览)
  • JEP 460: 向量API(Vector API,第七次孵化)
  • JEP 461: 流收集器(Stream Gatherers,预览)
  • JEP 462: 结构化并发(Structured Concurrency,第二次预览)
  • JEP 463: 隐式声明的类和实例主方法(Implicitly Declared Classes and Instance Main Methods,第二次预览)
  • JEP 464: 作用域值(Scoped Values,第二次预览)

接下来我们一起看看这些特性。

JEP 423: G1垃圾回收器的区域固定(Region Pinning for G1)

JEP 423: G1垃圾回收器的区域固定旨在解决在使用Java本地接口(JNI)时遇到的垃圾回收(GC)延迟问题。

在使用JNI时,Java线程需要等待GC操作完成,这会导致应用程序的延迟增加。特别是在JNI关键区域,GC操作会被暂停,直到线程离开该区域。这种机制虽然可以确保GC的稳定性,但会显著增加应用程序的延迟。

JEP 423通过引入区域固定机制来解决上述问题。具体来说,该特性允许在G1垃圾回收器中固定JNI代码使用的内存区域,这样即使在这些区域中存在GC操作,也不会影响到其他区域的垃圾回收。这通过以下方式实现:

  1. 区域计数器:在每个区域中维护一个计数器,用于记录该区域中的临界对象数量。当一个临界对象被获取时,计数器增加;当一个临界对象被释放时,计数器减少。
  2. 区域固定:在进行GC操作时,G1垃圾回收器会固定那些包含临界对象的区域,确保这些区域在GC期间保持不变。这样,即使线程处于JNI关键区域,垃圾回收也可以继续进行,而不会被暂停。

JEP 423可以带来显著的性能改进:

  1. 减少延迟:通过允许在JNI关键区域期间继续进行垃圾回收,减少了应用程序的延迟。
  2. 提高效率:Java线程无需等待GC操作完成,从而提高了开发人员的工作效率。
  3. 增强可预测性:该特性还增强了垃圾回收的可预测性,特别是在处理大对象时。

JEP 454: 外部函数和内存API(Foreign Function & Memory API)

FFM API是为了提供一个更安全、更高效的替代JNI(Java Native Interface)的API。JNI虽然允许Java程序调用本地代码,但其使用复杂且容易引入安全问题。FFM API旨在简化这一过程,提高开发者的生产力和体验,同时增强性能、安全性和一致性。在Java22中正式发布。

FFM API通过有效地调用外部函数(即JVM外部的代码)并安全地访问外部内存(即不受JVM管理的内存),使Java程序能够调用本机库并处理本机数据,而不会出现脆弱性和危险。

FFM API经历了多轮孵化和预览,从Java17的JEP 412开始,经过Java18的JEP 419和Java19的JEP 424,再到Java20的JEP 434和Java21的JEP 442,最终在Java22中正式发布。这些改进包括:

  • API的集中管理:通过Arena接口集中管理本地段的生命周期。
  • 安全访问外部内存:通过MemoryLayoutVarHandle操作和访问结构化的外部内存。
  • 调用外部函数:通过LinkerFunctionDescriptorSymbolLookup调用外部函数。

FFM API的主要收益包括:

  • 提高性能:通过直接调用本地函数和操作内存,提高了程序的执行效率。
  • 增强安全性:通过更严格的内存管理机制,减少了内存泄漏和安全漏洞的风险。
  • 提升开发体验:简化了与本地代码的交互,使得开发者可以更专注于业务逻辑的实现。

我们看下官方示例:


// 1. 在C库路径上查找名为radixsort的外部函数
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
final MemorySegment memorySegment = stdlib.find("radixsort").orElseThrow();
FunctionDescriptor descriptor = FunctionDescriptor.ofVoid(
        ValueLayout.ADDRESS,
        ValueLayout.JAVA_INT,
        ValueLayout.ADDRESS
);
MethodHandle radixsort = linker.downcallHandle(memorySegment, descriptor);

// 下面的代码将使用这个外部函数对字符串进行排序

// 2. 分配栈上内存来存储四个字符串
String[] javaStrings = {"mouse", "cat", "dog", "car"};
// 3. 使用try-with-resources来管理离堆内存的生命周期
try (Arena offHeap = Arena.ofConfined()) {
    // 4. 分配一段离堆内存来存储四个指针
    MemorySegment pointers = offHeap.allocateArray(ValueLayout.ADDRESS, javaStrings.length);
    // 5. 将字符串从栈上内存复制到离堆内存
    for (int i = 0; i < javaStrings.length; i++) {
        MemorySegment cString = offHeap.allocateUtf8String(javaStrings[i]);
        pointers.setAtIndex(ValueLayout.ADDRESS, i, cString);
    }
    // 6. 通过调用外部函数对离堆数据进行排序
    radixsort.invoke(pointers, javaStrings.length, MemorySegment.NULL, '\0');
    // 7. 将排序后的字符串从离堆内存复制回栈上内存
    for (int i = 0; i < javaStrings.length; i++) {
        MemorySegment cString = pointers.getAtIndex(ValueLayout.ADDRESS, i);
        javaStrings[i] = cString.getUtf8String(0);
    }
} // 8. 所有离堆内存在此处被释放

// 验证排序结果
assert Arrays.equals(javaStrings, new String[] {"car", "cat", "dog", "mouse"});  // true

我们都知道,JNI也是可以调用外部代码的,那FFM API相较于JNI的优势在于:

  1. 更安全的内存访问:FFM API 提供了一种更安全和受控的方式来与本地代码交互,避免了JNI中常见的内存泄漏和数据损坏问题。
  2. 直接访问本地内存:FFM API 允许Java程序直接访问本地内存(即Java堆外的内存),这使得数据处理更加高效和灵活。
  3. 跨语言函数调用:FFM API 支持调用Java程序的外部函数,以与外部代码和数据一起操作,而无需依赖JNI的复杂机制。
  4. 更高效的集成:FFM API 使得Java与C、C++等语言编写的库集成更加方便和高效,特别是在数据处理和机器学习等领域。
  5. 减少代码复杂性:FFM API 提供了一种更简洁的API,减少了JNI中复杂的代码编写和维护工作。
  6. 更广泛的适用性:FFM API 不仅适用于简单的函数调用,还可以处理复杂的内存管理任务,如堆外内存的管理。
  7. 提高性能:FFM API 通过高效的调用外部函数和安全地访问外部内存,提高了程序的运行效率。

JEP 456: 未命名变量和模式(Unnamed Variables & Patterns)

JEP 456: 未命名变量和模式(Unnamed Variables & Patterns)是一个重要新特性,在Java21中预览,在Java22中发布。旨在提高Java代码的可读性和可维护性。这一特性允许开发者在声明变量或嵌套模式时使用下划线字符(_)来表示未命名的变量或模式,从而简化代码并减少不必要的噪声,提高代码可读性和可维护性。

比如:

public static void main(String[] args) {
    var _ = new Point(1, 2);
}

record Point(int x, int y) {
}

这个可以用在任何定义变量的地方,比如:

  • ... instanceof Point(_, int y)
  • r instanceof Point _
  • switch …… case Box(_)
  • for (Order _ : orders)
  • for (int i = 0, _ = sideEffect(); i < 10; i++)
  • try { ... } catch (Exception _) { ... } catch (Throwable _) { ... }

只要是这个不准备用,可以一律使用_代替。

JEP 458: 启动多文件源代码程序(Launch Multi-File Source-Code Programs)

这个功能主要是提升java命令的能力。比如我们手搓了两个类:

// Prog.java
class Prog {
    public static void main(String[] args) { Helper.run(); }
}

// Helper.java
class Helper {
    static void run() { System.out.println("Hello!"); }
}

想要运行Prog的main方法,在JEP 458之前,我们需要先编译Helper,然后通过-classpath指令加载编译后的类,才能运行Prog。但是现在,java帮我们做了,我们直接使用java Prog.java就可以执行了。

很方便的一个功能,但是似乎好像大概看起来没什么用,但是对于Java生态有深远影响:

  1. 增强Java启动器功能:JEP 458允许Java启动器执行包含一个或多个文件的Java源码应用程序。这一改进使得开发者可以更灵活地启动和运行Java程序,特别是在需要处理多个源文件的复杂项目中,这一特性将大大提升开发效率和便利性。
  2. 促进Java生态系统的持续演进:JEP 458的引入是Java生态系统持续演进的一部分,与Java21的新特性和Quarkus的创新应用相辅相成。这些更新不仅提升了与现代Java生态系统的兼容性,还引入了领域模型验证的改进和依赖于Java17的新特性,为开发者提供了更加稳定、高效的工具集。
  3. 推动Java项目的发展:JEP 458的新特性被归类到四个主要的Java项目中,即Amber、Loom、Panama和Valhalla。这些项目旨在通过精巧的合并,孵化一系列组件,以便最终将其纳入到JDK中。这表明JEP 458不仅是一个独立的特性,而是Java生态系统中更广泛技术演进的一部分,有助于推动整个Java生态系统的创新和发展。
  4. 简化Java开发流程:通过简化启动多文件源码程序的过程,JEP 458有助于简化Java开发流程,使得开发者可以更加专注于业务逻辑的实现,而不是在启动和运行程序上花费过多时间。

预览功能

JEP 447: super(…)前置语句(Statements before super(…))

我们都知道,在子类的构造函数中,如果通过super(……)调用父类,在super之前是不允许有其他语句的。

大部分的时候这种限制都没问题,但是有时候不太灵活。如果想在super之前加上一些子类特有逻辑,比如想统计下子类构造耗时,就得重写一遍父类的实现。

除了有损灵活性,这种重写的做法也会造成父子类之间的关系变得奇怪。假设父类是SDK中的一个类,SDK升级时在父类构造函数增加了一些逻辑,我们项目中是无法继承这些逻辑的,某次需要升级SDK(比如低版本有安全风险),验证不完整的情况下,就很容易出现bug。

在 JEP 447 中,允许在构造函数中不引用正在创建的实例的语句出现在显式构造函数调用(如 super())之前。这一特性旨在为开发者提供更多的灵活性,允许在调用父类构造函数之前执行一些验证或其他处理操作。

引入这一特性的主要动机是提高构造函数的灵活性和可读性。通过允许在 super() 调用之前执行语句,开发者可以在构造函数中添加额外的逻辑,例如进行一些初始化检查或执行一些特定的处理操作,而不必依赖于 super() 调用之后的代码。

我们看下示例代码:

public class PositiveBigInteger extends BigInteger {
    public PositiveBigInteger(long value) {
        if (value <= 0) {
            throw new IllegalArgumentException("non-positive value");
        }
        super(value);
    }
}

JEP 457: 类文件API(Class-File API,预览)

Java中一直缺少官方的类文件操作API,想要操作class,我们需要借助第三方库,比如javassist、ASM、ByteBuddy等。从2017年开始,Java每半年有一次升级,特性更新频率增加,需要第三方库同步更新,是比较困难的。

还有一个原因是Java中使用了ASM实现jarjlink等工具,以及lambda表达式等。这就会出现一个问题,Java版本N依赖了ASM版本M,如果Java N中有类API,ASM M中是不会有的,只有Java N发布后,ASM升级到M+1才会有,Java想要使用ASM M+1,需要升级到Java N+1。是不是很颠,一个官方基础语言,居然要依赖一个依赖这个语言的第三方工具。是可忍孰不可忍。

于是有了JEP 457,目标是提供一个准确、完整、高性能且遵循Java虚拟机规范定义的类文件格式的API,最终也会替换JDK内部的ASM副本。

因为当前还是预览版,我们先简单看下官方示例:

如果要实现下面这段:

void fooBar(boolean z, int x) {
    if (z)
        foo(x);
    else
        bar(x);
}

我们在ASM的写法:

ClassWriter classWriter = ...;
MethodVisitor mv = classWriter.visitMethod(0, "fooBar", "(ZI)V", null, null);
mv.visitCode();
mv.visitVarInsn(ILOAD, 1);
Label label1 = new Label();
mv.visitJumpInsn(IFEQ, label1);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 2);
mv.visitMethodInsn(INVOKEVIRTUAL, "Foo", "foo", "(I)V", false);
Label label2 = new Label();
mv.visitJumpInsn(GOTO, label2);
mv.visitLabel(label1);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 2);
mv.visitMethodInsn(INVOKEVIRTUAL, "Foo", "bar", "(I)V", false);
mv.visitLabel(label2);
mv.visitInsn(RETURN);
mv.visitEnd();

在JEP 457中的写法:

ClassBuilder classBuilder = ...;
classBuilder.withMethod("fooBar", MethodTypeDesc.of(CD_void, CD_boolean, CD_int), flags,
                        methodBuilder -> methodBuilder.withCode(codeBuilder -> {
    Label label1 = codeBuilder.newLabel();
    Label label2 = codeBuilder.newLabel();
    codeBuilder.iload(1)
        .ifeq(label1)
        .aload(0)
        .iload(2)
        .invokevirtual(ClassDesc.of("Foo"), "foo", MethodTypeDesc.of(CD_void, CD_int))
        .goto_(label2)
        .labelBinding(label1)
        .aload(0)
        .iload(2)
        .invokevirtual(ClassDesc.of("Foo"), "bar", MethodTypeDesc.of(CD_void, CD_int))
        .labelBinding(label2);
        .return_();
});

还可以这样写:

CodeBuilder classBuilder = ...;
classBuilder.withMethod("fooBar", MethodTypeDesc.of(CD_void, CD_boolean, CD_int), flags,
                        methodBuilder -> methodBuilder.withCode(codeBuilder -> {
    codeBuilder.iload(codeBuilder.parameterSlot(0))
               .ifThenElse(
                   b1 -> b1.aload(codeBuilder.receiverSlot())
                           .iload(codeBuilder.parameterSlot(1))
                           .invokevirtual(ClassDesc.of("Foo"), "foo",
                                          MethodTypeDesc.of(CD_void, CD_int)),
                   b2 -> b2.aload(codeBuilder.receiverSlot())
                           .iload(codeBuilder.parameterSlot(1))
                           .invokevirtual(ClassDesc.of("Foo"), "bar",
                                          MethodTypeDesc.of(CD_void, CD_int))
               .return_();
});

写法上比ASM更加优雅。

JEP 459: 字符串模板(String Templates,第二次预览)

字符串模板是一个值得期待的功能,旨在增强Java编程语言。该特性通过将文字文本与嵌入式表达式和模板处理器结合,生成专门的结果,从而补充了Java现有的字符串文字和文本块。

相对Java21中JEP 430,主要的优化改动包括:

  • 增强的表达式支持:允许在字符串模板中嵌入更复杂的表达式,这些表达式可以在运行时被计算和校验。
  • 模板处理器:引入了模板处理器的概念,使得字符串模板可以更加灵活地处理不同的数据格式和结构。
  • 安全性提升:通过在运行时对嵌入的表达式进行校验,提高了代码的安全性。

字符串模板通过将文本和嵌入式表达式结合在一起,使得Java程序能够以一种更加直观和安全的方式构建字符串。与传统的字符串拼接(使用+操作符)、StringBuilderString.format 等方法相比,字符串模板提供了一种更加清晰和安全的字符串构建方式。特别是当字符串需要从用户提供的值构建并传递给其他系统时(例如,构建数据库查询),使用字符串模板可以有效地验证和转换模板及其嵌入表达式的值,从而提高Java程序的安全性。

让我们通过代码看一下这个特性的魅力:

public static void main(String[] args) {
    // 拼装变量
    String name = "看山";
    String info = STR. "My name is \{ name }" ;
    assert info.equals("My name is 看山");

    // 拼装变量
    String firstName = "Howard";
    String lastName = "Liu";
    String fullName = STR. "\{ firstName } \{ lastName }" ;
    assert fullName.equals("Howard Liu");
    String sortName = STR. "\{ lastName }, \{ firstName }" ;
    assert sortName.equals("Liu, Howard");

    // 模板中调用方法
    String s2 = STR. "You have a \{ getOfferType() } waiting for you!" ;
    assert s2.equals("You have a gift waiting for you!");

    Request req = new Request("2017-07-19", "09:15", "https://www.howardliu.cn");
    // 模板中引用对象属性
    String s3 = STR. "Access at \{ req.date } \{ req.time } from \{ req.address }" ;
    assert s3.equals("Access at 2017-07-19 09:15 from https://www.howardliu.cn");

    LocalTime now = LocalTime.now();
    String markTime = DateTimeFormatter
            .ofPattern("HH:mm:ss")
            .format(now);
    // 模板中调用方法
    String time = STR. "The time is \{
            // The java.time.format package is very useful
            DateTimeFormatter
                    .ofPattern("HH:mm:ss")
                    .format(now)
            } right now" ;
    assert time.equals("The time is " + markTime + " right now");

    // 模板嵌套模板
    String[] fruit = {"apples", "oranges", "peaches"};
    String s4 = STR. "\{ fruit[0] }, \{
            STR. "\{ fruit[1] }, \{ fruit[2] }"
            }" ;
    assert s4.equals("apples, oranges, peaches");

    // 模板与文本块结合
    String title = "My Web Page";
    String text = "Hello, world";
    String html = STR. """
    <html>
      <head>
        <title>\{ title }</title>
      </head>
      <body>
        <p>\{ text }</p>
      </body>
    </html>
    """ ;
    assert html.equals("""
            <html>
              <head>
                <title>My Web Page</title>
              </head>
              <body>
                <p>Hello, world</p>
              </body>
            </html>
            """);

    // 带格式化的字符串模板
    record Rectangle(String name, double width, double height) {
        double area() {
            return width * height;
        }
    }
    Rectangle[] zone = new Rectangle[] {
            new Rectangle("Alfa", 17.8, 31.4),
            new Rectangle("Bravo", 9.6, 12.4),
            new Rectangle("Charlie", 7.1, 11.23),
    };
    String table = FMT. """
        Description     Width    Height     Area
        %-12s\{ zone[0].name }  %7.2f\{ zone[0].width }  %7.2f\{ zone[0].height }     %7.2f\{ zone[0].area() }
        %-12s\{ zone[1].name }  %7.2f\{ zone[1].width }  %7.2f\{ zone[1].height }     %7.2f\{ zone[1].area() }
        %-12s\{ zone[2].name }  %7.2f\{ zone[2].width }  %7.2f\{ zone[2].height }     %7.2f\{ zone[2].area() }
        \{ " ".repeat(28) } Total %7.2f\{ zone[0].area() + zone[1].area() + zone[2].area() }
        """;
    assert table.equals("""
            Description     Width    Height     Area
            Alfa            17.80    31.40      558.92
            Bravo            9.60    12.40      119.04
            Charlie          7.10    11.23       79.73
                                         Total  757.69
            """);
}

public static String getOfferType() {
    return "gift";
}

record Request(String date, String time, String address) {
}

这个功能当前是第二次预览,Java23的8.12版本中还没有展示字符串模板的第三次预览(JEP 465: String Templates),还不能确定什么时候可以正式用上。

JEP 461: 流收集器(Stream Gatherers,预览)

JEP 461旨在增强Java Stream API、以支持自定义中间操作。这一特性允许开发者以更灵活和高效的方式处理数据流,从而提高流管道的表达能力和转换数据的能力。

流收集器通过引入新的中间操作Stream::gather(Gatherer),允许开发者定义自定义的转换实体(称为Gatherer),从而对流中的元素进行转换。这些转换可以是一对一、一对多、多对一或多对多的转换方式。此外,流收集器还支持保存以前遇到的元素,以便进行进一步的处理。

我们通过实例感受下这一特性的魅力:

public record WindowFixed<TR>(int windowSize) implements Gatherer<TR, ArrayList<TR>, List<TR>> {

    public static void main(String[] args) {
        var list = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                .gather(new WindowFixed<>(3))
                .toList();
        // [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
        System.out.println(list);
    }

    public WindowFixed {
        // Validate input
        if (windowSize < 1) {
            throw new IllegalArgumentException("window size must be positive");
        }
    }

    @Override
    public Supplier<ArrayList<TR>> initializer() {
        // 创建一个 ArrayList 来保存当前打开的窗口
        return () -> new ArrayList<>(windowSize);
    }

    @Override
    public Integrator<ArrayList<TR>, TR, List<TR>> integrator() {
        // 集成器在每次消费元素时被调用
        return Gatherer.Integrator.ofGreedy((window, element, downstream) -> {

            // 将元素添加到当前打开的窗口
            window.add(element);

            // 直到达到所需的窗口大小,
            // 返回 true 表示希望继续接收更多元素
            if (window.size() < windowSize) {
                return true;
            }

            // 当窗口已满时,通过创建副本关闭窗口
            var result = new ArrayList<TR>(window);

            // 清空窗口以便开始新的窗口
            window.clear();

            // 将关闭的窗口发送到下游
            return downstream.push(result);

        });
    }

    // 由于此操作本质上是顺序的,因此无法并行化,因此省略了合并器

    @Override
    public BiConsumer<ArrayList<TR>, Downstream<? super List<TR>>> finisher() {
        // 终结器在没有更多元素传递时运行
        return (window, downstream) -> {
            // 如果下游仍然接受更多元素且当前打开的窗口非空,则将其副本发送到下游
            if (!downstream.isRejecting() && !window.isEmpty()) {
                downstream.push(new ArrayList<TR>(window));
                window.clear();
            }
        };
    }
}

该特性还是预览版,等正式发布后再细说。

JEP 462: 结构化并发(Structured Concurrency,第二次预览)

结构化并发API(Structured Concurrency API)旨在简化多线程编程,通过引入一个API来处理在不同线程中运行的多个任务作为一个单一工作单元,从而简化错误处理和取消操作,提高可靠性,并增强可观测性。本次发布是第一次预览。

结构化并发API提供了明确的语法结构来定义子任务的生命周期,并启用一个运行时表示线程间的层次结构。这有助于实现错误传播和取消以及并发程序的有意义观察。

在JEP 462中,结构化并发API被进一步完善,使其更加易于使用和维护。主要的优化包括:

  • 工作单元概念:将一组相关任务视为一个工作单元,这样可以简化错误处理和取消操作。
  • 增强的可观测性:通过API提供了更好的错误处理机制和任务状态跟踪功能,增强了代码的可观察性。
  • 虚拟线程支持:与Project Loom项目结合,支持在线程内和线程间共享不可变数据,这在使用大量虚拟线程时尤其有用。

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 463: 隐式声明的类和实例主方法(Implicitly Declared Classes and Instance Main Methods,第二次预览)

无论学习哪门语言,第一课一定是打印Hello, World!,Java中的写法是:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println ("Hello, World!");
    }
}

如果是第一次接触,一定会有很多疑问,public干啥的,main方法的约定参数args是什么鬼?然后老师就说,这就是模板,照着抄就行,不这样写不运行。

JEP 445特性后,可以简化为:

class HelloWorld {
    void main() {
        System.out.println ("Hello, World!");
    }
}

我们还可以这样写:

String greeting() { return "Hello, World!"; }

void main() {
    System.out.println(greeting());
}

main方法直接简化为名字和括号,甚至连类也不需要显性定义了。虽然看起来没啥用,但是在JShell中使用,就比较友好了。

JEP 464: 作用域值(Scoped Values,第二次预览)

作用域值(Scoped Values)在Java20孵化,在Java21第一次预览,在Java22第二次预览,旨在提供一种安全且高效的方法来共享数据,无需使用方法参数。这一特性允许在不使用方法参数的情况下,将数据安全地共享给方法,优先于线程局部变量,特别是在使用大量虚拟线程时。

在多线程环境中,作用域值可以在线程内和线程间共享不可变数据,例如从父线程向子线程传递数据,从而解决了在多线程应用中传递数据的问题。此外,作用域值提高了数据的安全性、不变性和封装性,并且在多线程环境中使用事务、安全主体和其他形式的共享上下文的应用程序中表现尤为突出。

作用域值的主要特点:

  • 不可变性:作用域值是不可变的,这意味着一旦设置,其值就不能更改。这种不可变性减少了并发编程中意外副作用的风险。
  • 作用域生命周期:作用域值的生命周期仅限于 run 方法定义的作用域。一旦执行离开该作用域,作用域值将不再可访问。
  • 继承性:子线程会自动继承父线程的作用域值,从而允许在线程边界间无缝共享数据。

在这个功能之前,在多线程间传递数据,我们有两种选择:

  1. 方法参数:显示参数传递;缺点是新增参数时修改联动修改一系列方法,如果是框架或SDK层面的,无法做到向下兼容。
  2. ThreadLocal:在ThreadLocal保存当前线程变量。

使用过ThreadLocal的都清楚,ThreadLocal会有三大问题。

  1. 无约束的可变性:每个线程局部变量都是可变的。任何可以调用线程局部变量的get方法的代码都可以随时调用该变量的set方法。即使线程局部变量中的对象是不可变的,每个字段都被声明为final,情况仍然如此。ThreadLocal API允许这样做,以便支持一个完全通用的通信模型,在该模型中,数据可以在方法之间以任何方向流动。这可能会导致数据流混乱,导致程序难以分辨哪个方法更新共享状态以及以何种顺序进行。
  2. 无界生存期:一旦通过set方法设置了一个线程局部变量的副本,该值就会在该线程的生存期内保留,或者直到该线程中的代码调用remove方法。我们有时候会忘记调用remove,如果使用线程池,在一个任务中设置的线程局部变量的值如果不清除,可能会意外泄漏到无关的任务中,导致危险的安全漏洞(比如人员SSO)。对于依赖于线程局部变量的无约束可变性的程序来说,可能没有明确的点可以保证线程调用remove是安全的,可能会导致内存泄漏,因为每个线程的数据在退出之前都不会被垃圾回收。
  3. 昂贵的继承:当使用大量线程时,线程局部变量的开销可能会更糟糕,因为父线程的线程局部变量可以被子线程继承。(事实上,线程局部变量并不是某个特定线程的本地变量。)当开发人员选择创建一个继承了线程局部变量的子线程时,该子线程必须为之前在父线程中写入的每个线程局部变量分配存储空间。这可能会显著增加内存占用。子线程不能共享父线程使用的存储,因为ThreadLocal API要求更改线程的线程局部变量副本在其他线程中不可见。这也会有另一个隐藏的问题,子线程没有办法向父线程set数据。

作用域值可以有效解决上面提到的问题,而且写起来更加优雅。

我们一起看下作用域值的使用:

// 声明一个作用域值用于存储用户名
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 460: 向量API(Vector API,第七次孵化)

向量API的功能是提供一个表达向量计算的API,旨在通过引入向量计算API来提高Java应用程序的性能。这一API允许开发者在支持的CPU架构上可靠地编译为最佳向量指令,从而实现比等效的标量计算更高的性能。这些计算在运行时可靠地编译成支持的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中的独特优势在于其高效的并行计算能力、丰富的向量化指令集、跨平台的数据并行算法支持以及对机器学习的特别优化。

文末总结

本文介绍了 Java22 新增的特性,完整的特性清单可以从 https://openjdk.org/projects/jdk/22/ 查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。

青山不改,绿水长流,我们下次见。

推荐阅读


你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。

个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java22 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java22 的新特性

👇🏻欢迎关注我的公众号「看山的小屋」,领取精选资料👇🏻

公众号:看山的小屋