Java21 特性解读
2025-01-15 08:32 阅读(93)

当前 JDK 的版本已经到了 23 了,不过最近的 LTS 版本是 21,刚好最近准备把直播侧 serverless 应用的 JVM 环境升级到 java21(目前是 11),在升级前对 21 的特性做一个简单的了解和熟悉,下面是个人熟悉过程中的笔记,大家可以按照每一节特性中的代码自己在本地 run 下,可以更快地做个了解。




JDK 的版本其实最近几年开始,已经是 6 个月一个版本了,LTS 版本大概差不多间隔 4-6 个版本(不定),每次升级,都会有比较多的迭代,但是主要还是集中在几个方面:1. 新特性的支持,其实主要还是面向编写和阅读的自然语言化,做的新特性的提供或者语法糖的封装,突出易懂易用;2. 内部核心实现的性能或者能力的提升,感知比较多的是 gc,或者是内部的 hotspot 的能力等;3. bugfix,漏洞修复等。




LTS 版本还是值得去了解,有条件的话也是比较推荐在生产环境去做使用的,因为不管是上述哪个方面带来的提升,对开发以及系统运维来说,都是属于易得的红利。




介绍



▐  JEP



JEP 是 Java Enhancement Proposal 的简称,JEP 的提出到生效也需要一个过程,分别是:


1. Draft(草稿)阶段:需要明确提议的动机、描述、目标和非目标(nongoals)、风险等;


2. Submission(提交)阶段:该阶段会给该提议生成一个唯一的标识,叫 JAB number;


3. Review(审核)阶段:OpenJDK 社区中和该提议比较相关的 stakeholder 会参与 review,主要是看下这个提议对于 Java 的发展是否是必要、合理;


4. Sponsorship(赞助)阶段:这里主要是要有个开发团队去承接这个 JEP 的开发、测试和集成工作,该阶段还不需要实际投入开发;


5. Candidate Status(候选)阶段:进入到该阶段说明整个 JEP 完成立项,并且价值和作用得到了充分认同,然后是要等待在哪个版本去做试验接入;


6. Targeted(锁定版本)阶段:这个阶段已经为该 JEP 指定了一个 JDK 的版本,那需要把 JEP 对应的内容进行开发、测试;


7. Integrated(集成)阶段:将 JEP 对应的代码集成到具体版本的 JDK 源码中,刚集成到 JDK 源码中的特性一般都是 preview 状态,通常都需要至少 1 个版本迭代周期才能变成可被正常使用的状态,处于 preview 状态的 JEP,只能被试验性使用,不能被应用到生产环境,因为功能或者说特性可能在下一个版本中就会发生变化;


8. Released(发布)阶段:该特性可以面向整体 java 开发者做使用,我们平时能用上的特性都属于这个范畴。



 1. Feature:Unnamed Classes and Instance Main Methods (Preview)


动机:这个特性 java 推出比较早了,当时没有觉得学习门槛高,因为其他当时比较流行的语言,门槛更高,而且更难读,但是随着一代代发展下来,新出来的语音在语法和表达上,更接近于 NL,那一相比较,就显得 java 有点 “年龄大,水土不服”。

//package main.java.org.example;

void main(String[] args) {
    System.out.println("Hello world!");
}

TIPS:下文中特性后面括号带 Preview 的,都是前瞻的功能,需要使用的话得加上 --enable-preview:


javac --release 21 --enable-preview Main_TL.java

// 或在IDEA的VM参数中添加
--enable-preview

2. Feature: String Templates (Preview)



动机:更直观,简洁,方便维护,同时增加了值的验证和转换。


不少 JEP 都有 Non-Goals,这个看看还是蛮有趣的,希望是什么,不希望成为什么,这个特性的 Non-Goals 可以体会下:


非目标方向


- 不是面向原有的字符串操作 (+) 提供语法糖;


- 不是为了弃用或移除传统上用于复杂或程序化字符串组合的 StringBuilder 和 StringBuffer 类。

public class StringTemplate {
    static String anotherName = "Jiang";

    public static void main(String[] args) {
        String name = "Mario";
        String number = "987689";

        System.out.println(STR."name = \{name}, and number = \{number}, and anotherName = \{getAnotherName()}");
    }

    public static String getAnotherName() {
        return anotherName;
    }
}

3. Feature: Unnamed Patterns and Variables (Preview) & Record Patterns


(这两个 feature 比较关联的,就一起概述了)


动机:提升可读性,并且让程序员可以清楚知道哪些成员变量被使用,哪些成员变量未被使用,preview 的功能就是可以使用下划线来代替不想使用到的 record 里面的成员。

package org.example;

import java.util.List;

/**
 * 例子中sealed是java15给出的新特性,用于限制类继承,可以看做是枚举的扩展,java17中完善和标准化<br>
 * record是java14引入的新特性,用于创建不可变类,java16的时候成为正式标准
 */
public class UnnamedPatternsAndVariables {
    public static void main(String[] args) {
        process(List.of(new SealedClassA("A"), new SealedClassB("B", 123), new SealedClassC("C", 321, "hidden hobby")));
    }

    public static void process(List<SealedInterface> list) {
        for (SealedInterface sealedInterface : list) {
            if (sealedInterface instanceof SealedClassA(String name)) {
                System.out.println("The name of SealedClassA is " + name);
            } else if (sealedInterface instanceof SealedClassB(String name, Integer age)) {
                System.out.println("The name of SealedClassB is " + name + " and age is " + age);
            } else if (sealedInterface instanceof SealedClassC(String name, Integer age, String _)) {
                System.out.println("The name of SealedClassC is " + name + " and age is " + age + " and hobby is hidden");
            }
        }
    }

    public sealed interface SealedInterface permits SealedClassA, SealedClassB, SealedClassC {

    }

    public record SealedClassA(String name) implements SealedInterface {

    }

    public record SealedClassB(String name, Integer age) implements SealedInterface {

    }

    public record SealedClassC(String name, Integer age, String hobby) implements SealedInterface {

    }
}

4. Feature: Scoped Values (Preview)


特性目标:


易用性:提供一种编程模型,以便在一个线程内和子线程之间共享数据,从而简化对数据流的推理。


可理解性:使共享数据的生命周期能够从代码的语法结构中可见。


稳健性:确保调用者共享的数据只能被合法的被调用者检索。


性能:允许共享数据是不可变的,以便于被大量线程共享,并支持运行时优化。




可以重新绑定,面向之前通过上下文 context 传递的场景,以及使用 ThreadLocal 的场景;如果在中途使用异步线程进行额外操作处理,这里的值绑定会丢失,需要显示的在线程之间做传递。

package org.example;

import java.lang.ScopedValue;

public class ScopedValues {
    public static void main(String[] args) {
        ScopedValue.runWhere(EagleEye.TRACE_ID, "123456", () -> {
            RPCProcess rpcProcess = new RPCProcess();
            rpcProcess.process();
        });
    }
}

class EagleEye {
    public static final ScopedValue<String> TRACE_ID = ScopedValue.newInstance();
}

class RPCProcess {
    public void process() {
        System.out.println(STR."TRACE_ID: \{EagleEye.TRACE_ID.get()}, do rpc process");
        ServerService serverService = new ServerService();
        serverService.bizProcess();
    }
}

class ServerService {
    public void bizProcess() {
        System.out.println(STR."TRACE_ID: \{EagleEye.TRACE_ID.get()}, do biz process");
        AsyncProcessor asyncProcessor = new AsyncProcessor();
        asyncProcessor.asyncProc();
    }
}

class AsyncProcessor {
    public void asyncProc() {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(STR."TRACE_ID: \{!EagleEye.TRACE_ID.isBound() ? null : EagleEye.TRACE_ID.get()}, do async process");
            }
        });
        thread.start();
    }
}

5. Feature: Foreign Function & Memory API (Third Preview)


动机:为了 Java 程序员提供更可靠的操作 native 代码和存储(操作系统层面)的 API 能力。




JDK19 的时候首次提出,JDK20 第二版 preivew,JDK21 第三版 preview;该特性主要是为了提供一种更高效、便捷和安全的方式,去调用系统底层的类库,但是难点个人感觉还是在调用安全性上,这类工具在生产环境中的应用都需要比较慎重,因为都是属于运行时的错误,而且不容易测试发现。

package org.example;

import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;

public class ForeignFunctionMemory {
    public static void main(String[] args) {
        MemorySegment memorySegment = Arena.ofAuto().allocate(1024);

        memorySegment.set(ValueLayout.JAVA_BYTE, 0, (byte) 1);
        byte target = memorySegment.get(ValueLayout.JAVA_BYTE, 0);

        System.out.println(target);
    }
}

 6. Feature: Structured Concurrency (Preview)


动机:之前不同子的并发任务之间基本是独立,需要程序员自己去管理、组装以及显式调度,难免会出现一些线程泄漏(未正确关闭或回收线程)和取消延迟等问题;该特性是通过引入 Scope 的概念,简化 java 并发编程,把并发编程当成一个整体,把线程的管理和错误处理做了统一封装,让并发编程更傻瓜化,该特性在后续配合虚拟线程应该会有更广的应用。

package org.example;

import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

public class StructuredConcurrency {
    public static void main(String[] args) {

        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            Supplier<String> task1 = scope.fork(() -> {
                System.out.println("task1 is running...");
                TimeUnit.SECONDS.sleep(3);
                System.out.println("task1 is done.");
                return "Hello, Mario";
            });

            Supplier<Integer> task2 = scope.fork(() -> {
                System.out.println("task2 is running...");
                TimeUnit.SECONDS.sleep(1);
                System.out.println("task2 is done.");
                return 666;
            });

            scope.join().throwIfFailed();
            System.out.println("all tasks are done.");

            System.out.println("Task1 result:" + task1.get() + "\nTask2 result: " + task2.get());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

new StructuredTaskScope.ShutdownOnFailure () 默认使用的是虚拟线程,如果是要用普通的系统线程的话,可以调用另外一个构造方法,传入 ThreadFactory 即可。




▐  7. Feature: Vector API (Sixth Incubator)


引入一个 API,用于表示向量计算,该 API 能够在运行时可靠地编译为支持的 CPU 架构上的最佳向量指令,从而实现比等效的标量计算更优越的性能。第六版了,从 java16 推出,然后 17、18、19、20,到现在 21 都有做进一步的迭代孵化,现在依旧是个孵化阶段。




关注三个点:性能、可移植性(面向不同的 CPU 架构)、可靠性(即使在一些特殊的 CPU 架构下不能完美的利用硬件特性,但是也要保证在一定的效率下去正确的执行)。

package org.example;

import jdk.incubator.vector.IntVector;
import jdk.incubator.vector.VectorSpecies;

public class VectorAPIDemo {

    public static void main(String[] args) {
        int[] array1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int[] array2 = {11, 12, 13, 14, 15, 16, 17, 18, 19, 20};
        int[] result = new int[array1.length];

        VectorSpecies<Integer> vectorSpecies = IntVector.SPECIES_64;
        System.out.println("Vector Species Length: " + vectorSpecies.length());

        for (int i = 0; i < array1.length; i += vectorSpecies.length()) {
            System.out.println("Vector Loop, i: " + i);
            IntVector vector1 = IntVector.fromArray(vectorSpecies, array1, i);
            IntVector vector2 = IntVector.fromArray(vectorSpecies, array2, i);

            IntVector resultVector = vector1.add(vector2);
            resultVector.intoArray(result, i);
            System.out.printf("Result Vector: %s, and Result Array: %s%n", resultVector, toString(result));
        }

        for (int j : result) {
            System.out.println(j);
        }
    }

    public static String toString(int[] result) {
        StringBuilder sb = new StringBuilder();
        for (int j : result) {
            sb.append(j).append(",");
        }
        return sb.toString();
    }
}

javac --add-modules jdk.incubator.vector VectorAPI.java
java --add-modules jdk.incubator.vector VectorAPI.java

问题 1:这里的向量内部数据类型都是封装类型,主要受限于 java 的泛型机制,目前有个叫 Valhalla 的项目在推进增强 java 泛型的能力;

问题 2:对于 x64 中的 SIMD(是一种单指令多数据的并行处理技术)支持不太好,从运行性能上无法充分利用硬件特性;


问题 3:发现对于 mac 的 M 系列芯片也支持不太好,如果不是自身指定正确的 Species 的大小,都会出现 bound 越界的问题。




▐  8. Feature: Pattern Matching for switch


动机:增强 Switch 语法中对于数据类型的识别能力,提升 Switch 基于数据类型判断上的逻辑处理能力。

public static void main(String[] args) {
        System.out.println("Hello world!");

        System.out.println(formatterPatternSwitch(123)); // 输出: int 123
        System.out.println(formatterPatternSwitch(456L)); // 输出: long 456
        System.out.println(formatterPatternSwitch(789.0)); // 输出: double 789.000000
        System.out.println(formatterPatternSwitch("Hello")); // 输出: String Hello
        System.out.println(formatterPatternSwitch(new Object())); // 输出: java.lang.Object@<hashcode>
    }

    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();
        };
    }

 9. Feature: Sequenced Collections


动机:对于有序的集合或者 map 类型的操作支持不够好,同时差异化比较大,所以新增加几个超类,来定义针对这类有序集合的操作,做了一个统一。

interface SequencedCollection<E> extends Collection<E> {
    // new method
    SequencedCollection<E> reversed();
    // methods promoted from Deque
    void addFirst(E);
    void addLast(E);
    E getFirst();
    E getLast();
    E removeFirst();
    E removeLast();
}

interface SequencedMap<K,V> extends Map<K,V> {
    // new methods
    SequencedMap<K,V> reversed();
    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Entry<K,V>> sequencedEntrySet();
    V putFirst(K, V);
    V putLast(K, V);
    // methods promoted from NavigableMap
    Entry<K, V> firstEntry();
    Entry<K, V> lastEntry();
    Entry<K, V> pollFirstEntry();
    Entry<K, V> pollLastEntry();
}

public class SequencedCollections {
    public static void main(String[] args) {
        LinkedHashSet<Integer> hashSet = new LinkedHashSet<>();

        hashSet.add(10);
        hashSet.add(20);
        hashSet.add(30);
        hashSet.add(40);
        hashSet.add(50);
        hashSet.add(60);
        hashSet.add(70);
        hashSet.addFirst(0);
        hashSet.addLast(80);

        System.out.println(STR."hashSet = \{hashSet} , first = \{hashSet.getFirst()} , last = \{hashSet.getLast()}, reversed= \{hashSet.reversed()}");
    }
}

 10. Feature: Generational ZGC


动机:提升 ZGC 在面向不同 JVM 堆大小的性能和效率问题(之前是面向大堆有更好的性能收益,小堆可能未有明显收益或负向收益)。




ZGC 是在 java11 点时候推出的,从 java15 开始基本上可以在生产环境下使用;ZGC 的 STW 时间是微秒级,G1 是毫秒到秒级别;ZGC 目前将所有对象存储在一起,而不考虑对象的年龄,因此每次运行时都必须收集所有对象,但是年轻对象相比年老对象(old objects)生命周期更短,因此其实对于年轻对象增加 GC 的频次可以在效率以及内存回收收益上都是比较可观的。




G1 和 ZGC 都不属于传统意义上的分代垃圾回收器,但是 G1 里面还是通过监测和跟踪对象的存活周期做不同策略的回收,不过就目前看来这样的方式,都不如分代设计来的高效。




之前是 non-generational 的模式,就是面向一整块堆栈做操作,简单示意下:

这次的优化是增加一种叫 generational 的模式,非常类似之前的分代设计;

在 21 中要使用 ZGC 的话,在 JVM 启动参数里面要加入如下配置:


-XX:+UseZGC -XX:+ZGenerational


虚拟线程是轻量的,对资源诉求非常小的实现,不需要为虚拟线程去构建线程池,比较好的做法是快速使用,快速释放,短平快,虚拟线程替代不了原有的平台线程,但是在一些非 CPU 密集也不是 IO 密集的操作上会更合适,比如像处理网络类的请求,或者是一些非常简单的并发的任务。



package org.example;

import java.util.Scanner;

public class VirtualThreads implements Runnable{
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Use virtual threads? (true/false)");
        boolean useVirtual = scanner.nextBoolean();
        System.out.println("Use virtual threads: " + useVirtual);

        long start = System.currentTimeMillis();

        for (int i=0; i<100000; i++) {
            if (useVirtual) {
                Thread.startVirtualThread(new VirtualThreads());
            } else {
                Thread thread = new Thread(new VirtualThreads());
                thread.start();
            }
        }

        long end = System.currentTimeMillis();
        System.out.println("Time: " + (end - start));
    }

    @Override
    public void run() {
        // empty
    }
}

▐  12. Feature: Key Encapsulation Mechanism API



针对 KEM 提供相关的 API 和类库,KEM 是一种用于安全密钥交换的技术,目标是保障交互的双方的密钥约定生成和传输的安全性。

package org.example;

import javax.crypto.KEM;
import java.security.*;
import java.util.Arrays;
import java.util.Base64;

public class KEMDemo {
    public static void main(String[] args) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("X25519");
        KeyPair keyPair = keyPairGenerator.generateKeyPair();

        SecureRandom random = new SecureRandom();
        KEM kemS = KEM.getInstance("DHKEM");
        KEM.Encapsulated encapsulated = kemS.newEncapsulator(keyPair.getPublic(), random).encapsulate();
        byte[] encapsulatedKey = encapsulated.key().getEncoded();
        byte[] encapsulatedSymmetricKey = encapsulated.encapsulation();
        byte[] decryptedSymmetricKey = kemS.newDecapsulator(keyPair.getPrivate()).decapsulate(encapsulatedSymmetricKey).getEncoded();

        System.out.println("encapsulatedKey: " + Base64.getEncoder().encodeToString(encapsulatedKey));
        System.out.println("encapsulatedSymmetricKey: " + Base64.getEncoder().encodeToString(encapsulatedSymmetricKey));
        System.out.println("decryptedSymmetricKey: " + Base64.getEncoder().encodeToString(decryptedSymmetricKey));

        System.out.println("encapsulatedKey and decryptedSymmetricKey is equal ? " + Arrays.equals(encapsulatedKey, decryptedSymmetricKey));
    }
}

整体从 Feature 内容看,个人体感 STR 在使用上,还是提升不少便捷性的,在试用的时候也非常乐意去使用,期待后续尽早变成 release 状态;21 中给到比较大的提升,个人认为是虚拟线程和 ZGC,这两块后面升级后会做个应用实践,欢迎已经有实践经验的或者是有意向一起探索的小伙伴来交流。


来源: https://www.oschina.net/