1. 首页
  2. 后端

JDK 21 正式特性全面体验

  JDK 21 正式特性全面体验

===============

大家好,我是尘光,持续学习,持续创作中

序言

2023 年 9 月 19 日,Java 21 发布了!这篇文章通过示例来全面体验一下 Java 21 的正式特性

blog-jdk21-01.jpeg

这是自 2018 年 Java 10 发布以来,6 个月一个版本的第 12 个发行版。此外,这是继 Java 17 之后的又一个长期支持版本,Oracle 宣称至少会维护 8 年

新特性概览

Java 21 共有 15 个新特性,6 个预览版特性 + 1 个处于孵化阶段特性 + 8 个正式特性

| JEP 编号 | 特性中文名 | 特性英文名 | 特性阶段 | 分类 |
| — | — | — | — | — |
| 440 | Record 模式 | Record Patterns | 正式 | Amber 项目 |
| 441 | switch 模式匹配 | Pattern Matching for switch | 正式 | Amber 项目 |
| 430 | 字符串模板 | String Templates | 预览 | Amber 项目 |
| 443 | 匿名模式和变量 | Unnamed Pattern and Variable | 预览 | Amber 项目 |
| 445 | 匿名类和方法 | Unnamed Classes and Instance Main Methods | 预览 | Amber 项目 |
| 444 | 虚拟线程 | Virtual Threads | 正式 | Loom 项目 |
| 446 | 范围值 | Scoped Values | 预览 | Loom 项目 |
| 453 | 结构化并发 | Structured Concurrency | 预览 | Loom 项目 |
| 442 | 外部函数和内存 API | Foreign Function & Memory API | 预览 3 | Panama 项目 |
| 448 | 向量 API | Vector API | 孵化 6 | Panama 项目 |
| 431 | 有序集合接口 | Sequenced Collections | 正式 | Core |
| 439 | 分代 ZGC | Generational ZGC | 正式 | 性能提升 |
| 452 | 密钥封装机制 API | Key Encapsulation Mechanism API | 正式 | 性能提升 |
| 449 | 废弃 32 位 x86 架构 Windows 移植,待移除 | Deprecate the 32-bit x86 Port for Removal | 正式 | Other |
| 451 | 准备禁用代理的动态加载 | Prepare to Disallow the Dynamic Loading of Agents | 正式 | Other |

正式特性详解及代码实战

下载 IDEA 社区版最新版 2024.1 或 2024.2,下载 JDK 21(PS: 一台电脑上可以同时安装专业版和社区版)

blog-jdk21-02.jpg

在 IDEA 中新建一个工程 jdk-feature,选择已经下载好的 JDK21,Language Level 选择 21
oRVCb12NE7.jpg

JEP 431 – 有序集合接口

特性介绍

简而言之,在集合框架中增加一层描述顺序的抽象接口

在集合框架中新增了 SequencedCollection, SequencedSet, SequencedMap 接口

blog-jdk21-05.jpeg

在此之前,Collection 的核心子接口是 Set, ListQueue,但对于 SortedSetLinkedHashSet 来说,其实是有顺序的,和 List 接口有一些类似。此外,DequeList 中的元素有序,并且都支持从两端操作和遍历。但是他们的顶层父接口都是 Collection,从设计的角度来说,中间缺少一层抽象

SequencedCollection 定义的方法如下

public interface SequencedCollection<E> extends Collection<E> {
    // 新引入的方法
    SequencedCollection<E> reversed();

    // 从 Deque 迁移的方法
    void addFirst(E);
    void addLast(E);
    E getFirst();
    E getLast();
    E removeFirst();
    E removeLast();
}

可以看到,主要是定义了在两端添加、删除元素,获取两端的元素,以及 reversed() 方法。reversed() 方法返回原始集合从后往前结构的视图,修改原始集合是否对视图可见要取决于具体实现

同理,SequencedMap 也定义了从两端添加、删除元素,获取两端元素的方法

public interface SequencedMap<K,V> extends Map<K,V> {
    // 新引入的方法
    SequencedMap<K,V> reversed();
    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Entry<K,V>> sequencedEntrySet();
    V putFirst(K, V);
    V putLast(K, V);

    // 从 NavigableMap 迁移的方法
    Entry<K, V> firstEntry();
    Entry<K, V> lastEntry();
    Entry<K, V> pollFirstEntry();
    Entry<K, V> pollLastEntry();
}

Java 21 实现的 4 个默认顺序集合接口视图如下

  • ReverseOrderDequeView
  • ReverseOrderListView
  • ReverseOrderSortedMapView
  • ReverseOrderSortedSetView

实战示例

实例化一个 ArrayList 后立即创建对应的 reverseOrder 视图,然后在原始集合上操作,这些操作会反映在视图上,最终倒序打印结果

SequencedCollection<Integer> list = new ArrayList<>(List.of(3, 6, 9, 12));
SequencedCollection<Integer> reversed = list.reversed();
list.removeFirst();
list.addLast(15);

// class java.util.ArrayList
System.out.println(list.getClass());

// class java.util.ReverseOrderListView$Rand
System.out.println(reversed.getClass());

// 15 12 9 6
reversed.forEach(System.out::println);

同样地,实例化一个 LinkedHashSet 后立即创建对应的 reverseOrder 视图,然后在原始集合上操作,这些操作同样会反映在视图上,最终倒序打印结果

SequencedSet<Integer> set = new LinkedHashSet<>();
SequencedSet<Integer> reversed = set.reversed();
set.addLast(3);
set.addLast(6);
set.addLast(9);
set.addLast(12);
set.removeFirst();

// class java.util.LinkedHashSet
System.out.println(set.getClass());

// class java.util.LinkedHashSet$1ReverseLinkedHashSetView
System.out.println(reversed.getClass());

// 12, 9, 6
reversed.forEach(System.out::println);

同样地,实例化一个 LinkedHashMap 后立即创建对应的 reverseOrder 视图,然后在原始集合上操作,这些操作同样会反映在视图上,最终倒序打印结果

SequencedMap<Integer, Integer> map = new LinkedHashMap<>();
SequencedMap<Integer, Integer> reversed = map.reversed();
map.putLast(3, 1);
map.putLast(6, 2);
map.putLast(9, 3);
map.putLast(12, 4);
map.pollFirstEntry();

// class java.util.LinkedHashMap
System.out.println(map.getClass());

// class java.util.LinkedHashMap$ReversedLinkedHashMapView
System.out.println(reversed.getClass());

// 12:4, 9:3, 6:2
reversed.forEach((k, v) -> System.out.println(k + ":" + v));

JEP 439 – 分代 ZGC

特性介绍

简而言之,ZGC 支持分代收集,更频繁地收集新生代对象

ZGC 在 Java 11 作为实验特性,在 Java 15 作为正式特性,标志着 ZGC 已经在生产环境可用了

ZGC 拥有极致的性能,有几个重要特性

  1. 并发收集,并且使用了染色指针、读屏障等技术
  2. 低延迟,承诺停顿时间小于 1ms (刚开始引入时是 10ms)
  3. 基于 Region,支持的堆内存范围是 16MB ~ 16TB
  4. 与 G1 相比,ZGC 对应用程序吞吐量的影响小于 15%

不过,在 Java 21 之前,ZGC 是不分代的,不区分新生代和老年代,都使用同一套回收算法。虽然整体性能很出色,但是仍然有性能优化的空间,因为新生代的对象存活时间较短,具有更高的回收价值

Java 21 对此做了性能优化,实现了 ZGC 的分代收集,更频繁地对新生代进行垃圾收集。在 ZGC 的基础上,分代 ZGC 有以下新特性

  1. 引入了写屏障技术
  2. 分配停顿风险更小
  3. 堆内存开销更小
  4. GC CPU 开销更小

目前,分代 ZGC 是更好的解决方案,适用于绝大多数场景。在后续的版本中,非分代 ZGC 会逐渐废弃,以减少维护成本。由于直接实现分代 ZGC 技术难度较大,而承诺产品发布周期比较紧凑,因此先使用非分代 ZGC
作为过渡版本。这是一个重要的产品思维:「先有功能,再做优化

实战示例

准备工作

可以在 IDEA 菜单栏 Run/Debug Configurations 中配置 JVM 参数

blog-jdk21-08.jpeg

由于要观察 JVM 内存变化情况,可以下载 VisualVM,打开后依次点击菜单栏 Tools / Plugins 下载 Visual GC 插件

6uTaAVGYK8.jpg

以下代码构造了 SmallObjectHugeObject,循环 30000 次,共创建 30000 个 SmallObject 对象,6 个 HugeObject 对象,大对象保存到静态容器 HUGE_CACHE 中,防止被回收

public class Jep439Demo {

  private static final List<HugeObject> HUGE_CACHE = new ArrayList<>();

  static class SmallObject {
    String name;
    byte[] arr;
    public SmallObject(String name) {
      this.name = name;
      this.arr = new byte[16 * 1024];
      for (int i = 0; i < this.arr.length; i++) {
        this.arr[i] = (byte) (i % 256);
      }
    }
    public String getName() {
      return name;
    }
  }

  static class HugeObject {
    byte[] arr;
    public HugeObject() {
      this.arr = new byte[3 * 1024 * 1024];
      for (int i = 0; i < this.arr.length; i++) {
        this.arr[i] = (byte) (i % 256);
      }
    }
  }

  public static void main(String[] args) throws Exception {
    // Wait for monitoring in VisualVM.
    Thread.sleep(20_000);

    List<String> list = new ArrayList<>();
    Random random = new Random();
    for (int i = 1; i <= 30000; i++) {

      SmallObject item = new SmallObject("obj" + i);
      list.add(item.getName());
      if (i % 5000 == 0) {
        HUGE_CACHE.add(new HugeObject());
      }

      if (i % 100 == 0) {
        System.out.printf("Round %s, small.size()=%s, huge.size()=%s%n", i, list.size(), HUGE_CACHE.size());
      }
      Thread.sleep(10 + random.nextInt(10));
    }

    // Waiting for 12 seconds.
    Thread.sleep(12_000);
  }
}

由于 VisualVM 里的 Visual GC 插件暂时不支持 ZGC,直接分析 GC 日志文件

ZGC – 开启分代

配置 JVM 参数,运行并在 VisualVM 中监控

-Xmx100m
-Xlog:gc:file=/Users/jason315/Desktop/gc/zgc-gen.log
-Xlog:gc*:file=/Users/jason315/Desktop/gc/zgc-gen-detail.log
-XX:+UseZGC
-XX:+ZGenerational

相对于非分代 ZGC,特殊的参数是 -XX:+ZGenerational,表示开启分代模式。这里没有添加 JVM 堆的初始大小参数 -Xms100m,是为了观察 ZGC 的一个重要特性:内存自动整理

VisualVM 统计的内存趋势图如下。程序结束前,已用的 JVM 内存为 45M 左右。可以看到,当 ZGC 分析出前一段时间的内存使用率较低时,主动降低了堆内存,之后再根据已用内存自动调整堆内存

blog-jdk21-zgc-generational-m.jpeg

重要 GC 日志如下,算上预热,共有 66 次 GC,GC 总时间为 3215ms,其中新生代总时间为 1161ms,老年代总时间为 2054ms
blog-jdk21-zgc-generational-log4.jpeg

blog-jdk21-zgc-generational-log1.jpeg

blog-jdk21-zgc-generational-log2.jpeg

blog-jdk21-zgc-generational-log3.jpeg

通过分析 GC 日志可以看出,每次 GC 时,先打印了 GC(n) Y: xxx 日志,然后打印了 GC(n) O: xxx 日志,同时触发了新生代和老年代垃圾收集

程序退出前,打印了 ZGC 统计信息,从中摘取了新生代和老年代停顿时间信息,以最坏的情况考虑,每次 GC 时,停顿时间都远远小于 1ms

[535.801s][info][gc,stats    ]                                                                    Last 10s              Last 10m              Last 10h                Total
[535.801s][info][gc,stats    ]                                                                    Avg / Max             Avg / Max             Avg / Max             Avg / Max
[535.801s][info][gc,stats    ]         Old Pause: Pause Mark End                                0.000 / 0.000         0.010 / 0.020         0.010 / 0.020         0.010 / 0.020       ms
[535.801s][info][gc,stats    ]         Old Pause: Pause Relocate Start                          0.000 / 0.000         0.008 / 0.019         0.008 / 0.019         0.008 / 0.019       ms
[535.802s][info][gc,stats    ]       Young Pause: Pause Mark End                                0.000 / 0.000         0.014 / 0.023         0.014 / 0.023         0.014 / 0.023       ms
[535.802s][info][gc,stats    ]       Young Pause: Pause Mark Start                              0.000 / 0.000         0.000 / 0.000         0.000 / 0.000         0.000 / 0.000       ms
[535.802s][info][gc,stats    ]       Young Pause: Pause Mark Start (Major)                      0.000 / 0.000         0.018 / 0.066         0.018 / 0.066         0.018 / 0.066       ms
[535.802s][info][gc,stats    ]       Young Pause: Pause Relocate Start                          0.000 / 0.000         0.012 / 0.094         0.012 / 0.094         0.012 / 0.094       ms

ZGC – 不开启分代

配置 JVM 参数,运行并在 VisualVM 中监控

-Xmx100m
-Xlog:gc:file=/Users/jason315/Desktop/gc/zgc.log
-Xlog:gc*:file=/Users/jason315/Desktop/gc/zgc-detail.log
-XX:+UseZGC

同样地,这里没有添加 JVM 堆的初始大小参数 -Xms100m,是为了观察 ZGC 的一个重要特性:内存自动整理

VisualVM 统计的内存趋势图如下。程序结束前,已用的 JVM 内存为 40M 左右。可以看到,当 ZGC 分析出前一段时间的内存使用率较低时,主动降低了堆内存,之后再根据已用内存自动调整堆内存。可以看出,已用内存趋势图要平滑一些

blog-jdk21-zgc-non-generational-memory.jpeg

重要 GC 日志如下,算上预热,共有 62 次 GC
blog-jdk21-zgc-non-generational-log1.jpeg

blog-jdk21-zgc-non-generational-log2.jpeg

同样地,从中摘取停顿时间信息,以最坏的情况考虑,每次 GC 时,停顿时间也远远小于 1ms

[550.055s][info][gc,stats    ]                                                              Last 10s              Last 10m              Last 10h                Total
[550.055s][info][gc,stats    ]                                                              Avg / Max             Avg / Max             Avg / Max             Avg / Max
[550.055s][info][gc,stats    ]       Phase: Pause Mark End                                0.011 / 0.011         0.014 / 0.065         0.014 / 0.065         0.014 / 0.065       ms
[550.055s][info][gc,stats    ]       Phase: Pause Mark Start                              0.009 / 0.009         0.010 / 0.065         0.010 / 0.065         0.010 / 0.065       ms
[550.055s][info][gc,stats    ]       Phase: Pause Relocate Start                          0.005 / 0.005         0.006 / 0.011         0.006 / 0.011         0.006 / 0.011       ms
[550.055s][info][gc,stats    ]    Subphase: Pause Mark Try Complete                       0.000 / 0.000         0.001 / 0.001         0.001 / 0.001         0.001 / 0.001       ms

G1

配置 JVM 参数,运行并在 VisualVM 中监控

-Xmx100m
-Xlog:gc:file=/Users/jason315/Desktop/gc/g1.log
-Xlog:gc*:file=/Users/jason315/Desktop/gc/g1-detail.log
-XX:+UseG1GC

VisualVM 统计的内存趋势图如下。程序结束前,已用的 JVM 内存为 55M 左右,峰值内存达到了 80+M,相比于 ZGC,变化幅度较大

blog-jdk21-g1-m.jpeg

Visual GC 统计信息如下,算上预热,共有 10 次 GC,GC 总时间为 191.213ms
blog-jdk21-g1-visualgc.jpeg

最后一次 GC 日志,可以看出,停顿时间为 18.754ms,比 ZGC 差了不止一个量级
blog-jdk21-g1-log.jpeg

JEP 440 – Record 模式

特性介绍

简而言之,instanceof 关键字后支持 Records,简化 Records 对象值和方法的访问

Java 16 引入了一个正式特性: Records,表示一种特殊的类,极大地简化了不可变类的定义。如以下代码所示,Point 类有 2 个 final 属性,分别为 xy

record Point(int x, int y) {}

Java 16 还扩展了 instanceof 关键字,引入 Type 模式,在 if 语句块内使用变量时,不用强制类型转换了

public static void testInstanceOf(Object o) {
    // Java 16 以前
    if (o instanceof String) {
        String str = (String) o;
        // ... 强制类型转换后才能使用 str ...
    }

    // Java 16 及以上
    if (o instanceof String str) {
        // ... 直接使用 str ...
    }
}

JEP440 扩展了模式匹配以表示更复杂,组合更灵活的数据访问方式,极大提高了开发效率

Record 模式增强了 Java 编程语言以便于解构 Record 对象的值,这可以嵌套 Record 模式和 Type 模式,实现强大的、声明式的和可组合的数据处理形式

Record 模式可以和 switch 模式匹配结合使用,会在接下来的 JEP441 中介绍

实战示例

来自 JEP440 的示例如下,很明显 printSumInJava21 的写法更简洁。输出结果均为 9

public class Jep440Demo {
  record Point(int x, int y) {}

  // Java 16 写法
  static void printSumInJava16(Object obj) {
    if (obj instanceof Point p) {
      int x = p.x();
      int y = p.y();
      System.out.println(x+y);
    }
  }

  // Java 21 写法
  static void printSumInJava21(Object obj) {
    if (obj instanceof Point(int x, int y)) {
      System.out.println(x+y);
    }
  }

  public static void main(String[] args) {
    Point point = new Point(3, 6);

    printSumInJava16(point);
    printSumInJava21(point);
  }
}

同样地,Record 模式支持嵌套的 Records 解构赋值

public class Jep440NestedDemo {
  record Point(int x, int y) {}
  enum Color { RED, GREEN, BLUE }
  record ColoredPoint(Point p, Color c) {}
  record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}

  static void printRectangleInfoInJava21(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
      int area = Math.abs(lr.p().x() - ul.p().x()) * Math.abs(lr.p().y() - ul.p().y());
      String template = "Rectangle upper-left color is %s, lower-right color is %s.";
      System.out.printf((template) + "%n", ul.c(), lr.c());
      System.out.printf("Rectangle area is %d.", area);
    }
  }

  public static void main(String[] args) {
    Point ulPoint = new Point(0, 6);
    ColoredPoint ulColorPoint = new ColoredPoint(ulPoint, Color.RED);
    Point lrPoint = new Point(12, 0);
    ColoredPoint lrColorPoint = new ColoredPoint(lrPoint, Color.BLUE);
    Rectangle rectangle = new Rectangle(ulColorPoint, lrColorPoint);

    printRectangleInfoInJava21(rectangle);
  }
}

执行结果如下

Rectangle upper-left color is RED, lower-right color is BLUE.
Rectangle area is 72.

JEP 441 – switch 模式匹配

特性介绍

简而言之,switch 表达式支持更多的类型,并且支持模式匹配,自动解构赋值

经过 4 次预览后,switch 模式匹配终于在 Java 21 成为正式特性。从此,Java switch 语法更加完善,更加易用

在之前的 4 次预览中已经介绍过,switch 表达式的核心语法如下

  1. 增强类型检查,case 表达式支持更多类型,包括 Records

对于有父子关系的多个子句,如果父类型在子类型之前,父类型子句会优先匹配,子类型子句将不可达,会抛出编译期错误。此时应该调换一下顺序
2. switch 表达式和语句分支全覆盖检测
3. 扩展了模式变量声明范围

* 任意的 when 语句
* case 语句箭头后的表达式、代码块、throw 语句
* 一个 case 语句的模式变量范围,不允许越过另一个 case 语句
  1. 优化 null 处理,可以声明一个 null case

在 JEP441 中,与前面的预览 JEP 相比,有 2 个新变化

  • 去掉了括号模式,因为不存在有效值,无使用场景
  • case 语句支持限定枚举常量

实战示例

JEP441 中提供的一个示例

public class Jep441Demo {
  record Point(int i, int j) {}
  enum Color { RED, GREEN, BLUE; }

  static void typeTester(Object obj) {
    switch (obj) {
      // case 语句中支持 null
      case null     -> System.out.println("null");
      // case 语句中支持模式匹配,在后面的语句中可以直接使用 s
      case String s -> System.out.println("String is: " + s);
      // case 语句支持枚举模式匹配
      case Color c  -> System.out.println("Color: " + c.toString());
      // case 语句支持 Record 模式,前面已经介绍过
      case Point p  -> System.out.println("Record class: " + p.toString());
      case int[] ia -> System.out.println("Array length" + ia.length);
      default       -> System.out.println("Something else");
    }
  }
}

通常,一个 case 分支中还会细分不同的处理逻辑

// 不推荐写法
static void testStringOld(String response) {
    switch (response) {
        case null -> { }
        case String s -> {
            if (s.equalsIgnoreCase("YES"))
                System.out.println("You got it");
            else if (s.equalsIgnoreCase("NO"))
                System.out.println("Shame");
            else
                System.out.println("Sorry?");
        }
    }
}

// 推荐写法,可读性更强
static void testStringNew(String response) {
  switch (response) {
    case null -> { }
    case String s when s.equalsIgnoreCase("YES") -> System.out.println("You got it");
    case String s when s.equalsIgnoreCase("NO") -> System.out.println("Shame");
    case String s -> System.out.println("Sorry?");
  }
}

Java 21 中的 case 语句支持枚举常量,如以下代码所示

public class Jep441EnumDemo {
  sealed interface CardClassification permits Suit, Tarot {}
  public enum Suit implements CardClassification { CLUBS, DIAMONDS, HEARTS, SPADES }
  static final class Tarot implements CardClassification {}

  // Java 21 之前
  static void exhaustiveSwitchWithoutEnumSupport(CardClassification c) {
    switch (c) {
      case Suit s when s == Suit.CLUBS -> System.out.println("It's clubs");
      case Suit s when s == Suit.DIAMONDS -> System.out.println("It's diamonds");
      case Suit s when s == Suit.HEARTS -> System.out.println("It's hearts");
      case Suit s -> System.out.println("It's spades");
      case Tarot t -> System.out.println("It's a tarot");
    }
  }

  // Java 21 及以后
  static void exhaustiveSwitchWithBetterEnumSupport(CardClassification c) {
    switch (c) {
      case Suit.CLUBS -> System.out.println("It's clubs");
      case Suit.DIAMONDS -> System.out.println("It's diamonds");
      case Suit.HEARTS -> System.out.println("It's hearts");
      case Suit.SPADES -> System.out.println("It's spades");
      case Tarot t -> System.out.println("It's a tarot");
    }
  }
}

JEP 444 – 虚拟线程

特性介绍

简而言之,Java 正式支持轻量级线程

虚拟线程无疑是 Java 21 最受关注的正式特性。虚拟线程,也就是轻量级线程。虚拟线程极大地降低了高吞吐量应用的开发和维护成本

平台线程(原有线程)是在 OS 线程上做的封装,它的创建和切换成本很高,可用的线程数量也有限制。对于并发较高的应用,想要提高系统的吞吐量,之前一般是做异步化,但这种方式很难定位线上问题

虚拟线程的资源分配和调度由 Java 平台实现,它不再直接与 OS 线程强关联,而是直接将平台线程作为载体线程,这使得虚拟线程的可用数量大大增加

每个请求一个线程

虚拟线程的引入,让「每个请求一个线程」风格再次回到开发者的视线。这是一种常见的 Web 应用程序架构模式,用于处理并发请求。在这种模式下,每个传入的请求(如 HTTP 请求)都会分配一个独立的线程来处理,直到该请求完成为止。一旦请求处理完成,线程就会释放,可以被用于处理其他请求

这种模式的主要优点包括:

  1. 逻辑简单

每个请求的处理逻辑都在自己的线程中运行,避免了复杂的并发控制和同步问题
2. 快速响应

由于每个请求都在单独的线程中处理,因此可以快速响应请求,而不受其他请求的影响
3. 易于开发

开发者可以按照顺序编程的方式编写代码,而不必担心线程安全问题

虚拟线程应用场景

虚拟线程在如下两种场景下,才能大幅提高应用系统的吞吐量

  • 并发任务量很大(万级)
  • 线程工作量不会使 CPU 受限(不是 CPU 密集型任务)

虚拟线程创建方式

共有 5 中创建虚拟线程的方式

class Task implements Runnable{
  @Override
  public void run() {
    Thread currentThread = Thread.currentThread();
    long threadId = currentThread.threadId();
    System.out.printf("[%d]Task start to run...\n", threadId);
    int sum = IntStream.range(0, 100).sum();
    System.out.printf("[%d]sum=%d, 线程名称: '%s', 是否虚拟线程: %s\n",
            threadId, sum, currentThread.getName(), currentThread.isVirtual());
    System.out.printf("[%d]Task end.\n", threadId);
  }
}

// 方式一:直接启动,虚拟线程名称为空
private void way1() {
  // 等价于 Thread.ofVirtual().start(new Task());
  Thread.startVirtualThread(new Task());
}

// 方式二:Builder 模式构建
private void way2() {
  Thread vt = Thread.ofVirtual()
          .name("VirtualWorker-", 1)
          .unstarted(new Task());
  vt.start();
}

// 方式三:Factory 模式构建
private void way3() {
  ThreadFactory factory = Thread.ofVirtual()
          .name("VirtualFactoryWorker-", 1)
          .factory();
  Thread vt = factory.newThread(new Task());
  vt.start();
}

// 方式四:newVirtualThreadPerTaskExecutor
private void way4() {
  try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(new Task());
  }
}

// 方式五:构建"虚拟线程池"
private void way5() {
  ThreadFactory factory = Thread.ofVirtual()
          .name("VirtualFactoryWorker-", 1)
          .factory();
  ExecutorService executorService = Executors.newThreadPerTaskExecutor(factory);
  executorService.submit(new Task());
}

实战示例

示例 1

基于上述创建虚拟线程的 5 中方法,进行如下测试

public class Jep444CreateWaysDemo {
  private static final List<String> LOGS = new CopyOnWriteArrayList<>();

  static class Task implements Runnable{
    @Override
    public void run() {
      Thread currentThread = Thread.currentThread();
      long threadId = currentThread.threadId();
      LOGS.add("[%d]Step 1, task start to run...".formatted(threadId));
      int sum = IntStream.range(0, 100).sum();
      LOGS.add("[%d]Step 2, sum=%d, 线程名称: '%s', 是否虚拟线程: %s".formatted(threadId, sum, currentThread.getName(), currentThread.isVirtual()));
      LOGS.add("[%d]Step 3, task end.".formatted(threadId));
    }
  }

  // 方式一:直接启动,虚拟线程名称为空
  private static void way1() {
    // 等价于 Thread.ofVirtual().start(new Task());
    Thread.startVirtualThread(new Task());
  }

  // 方式二:Builder 模式构建
  private static void way2() {
    Thread vt = Thread.ofVirtual()
        .name("VirtualWorker-", 1)
        .unstarted(new Task());
    vt.start();
  }

  // 方式三:Factory 模式构建
  private static void way3() {
    ThreadFactory factory = Thread.ofVirtual()
        .name("VirtualFactoryWorker-", 1)
        .factory();
    Thread vt = factory.newThread(new Task());
    vt.start();
  }

  // 方式四:newVirtualThreadPerTaskExecutor
  private static void way4() {
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
      executor.submit(new Task());
    }
  }

  // 方式五:构建"虚拟线程池"
  private static void way5() {
    ThreadFactory factory = Thread.ofVirtual()
        .name("VirtualFactoryWorker-", 1)
        .factory();
    try (ExecutorService executorService = Executors.newThreadPerTaskExecutor(factory)) {
      executorService.submit(new Task());
    }
  }

  public static void main(String[] args) throws Exception {
    way1();
    way2();
    way3();
    way4();
    way5();
    Thread.sleep(5_000);
    // 由于多线程之间是无序的,对结果进行排序,方便展示
    LOGS.stream().sorted().forEach(System.out::println);
  }
}

运行结果如下,结果显示线程都是虚拟线程

[21]Step 1, task start to run...
[21]Step 2, sum=4950, 线程名称: '', 是否虚拟线程: true
[21]Step 3, task end.

[23]Step 1, task start to run...
[23]Step 2, sum=4950, 线程名称: 'VirtualWorker-1', 是否虚拟线程: true
[23]Step 3, task end.

[25]Step 1, task start to run...
[25]Step 2, sum=4950, 线程名称: 'VirtualFactoryWorker-1', 是否虚拟线程: true
[25]Step 3, task end.

[27]Step 1, task start to run...
[27]Step 2, sum=4950, 线程名称: '', 是否虚拟线程: true
[27]Step 3, task end.

[31]Step 1, task start to run...
[31]Step 2, sum=4950, 线程名称: 'VirtualFactoryWorker-1', 是否虚拟线程: true
[31]Step 3, task end.

示例 2

创建 100 万个虚拟线程

public class Jep444HugeThreadsDemo {
  private static void createHugeThreads() {
    // 创建 100 万个虚拟线程
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
      IntStream.range(0, 1000_000).forEach(i -> {
        executor.submit(() -> {
          Thread.sleep(Duration.ofSeconds(1));
          return i;
        });
      }); // try-with-resources,会隐式调用 executor.close()
    }
  }

  public static void main(String[] args) throws Exception {
    // Wait for VisualVM to monitor.
    Thread.sleep(10_000);

    System.out.println("Start to create virtual threads...");
    long startTime = System.currentTimeMillis();
    createHugeThreads();
    System.out.printf("Create huge virtual threads finished, cost %d ms.", System.currentTimeMillis() - startTime);

    // Wait for 5 seconds.
    Thread.sleep(5_000);
  }
}

配置 JVM 参数后执行,并且在 VisualVM 中观察

-Xmx3072m
-XX:+UseZGC
-XX:+ZGenerational

运行结果如下,可以很轻松地创建 100 个虚拟线程

Start to create virtual threads...
Create huge virtual threads finished, cost 7395 ms.

VisualVM 统计信息如下,最大堆内存不到 1250M

blog-jdk21-virtual-thread1.jpeg

其中有一个 VirtualThread-unparker 线程较为特殊
blog-jdk21-virtual-thread2.jpeg

JEP 449 – 废弃 32 位 x86 架构 Windows 移植

简而言之,准备不支持在 32 位 x86 架构 Windows 上运行了

随着计算机硬件的飞速发展,64 位系统逐渐普及。支持 32 位的最后一个 Windows 操作系统,Windows 10 也仅会支持到 2025 年 10 月

基于以上原因,Java 21 宣布废弃对 32 位 x86 架构 Windows 的移植,并在以后的版本中彻底移除(JEP479)。这样一来,丢下了一些包袱,OpenJDK 社区将腾出更多的精力专注于开发其他功能。在此之前,实现虚拟线程时还对此做了兼容

当尝试在 32 位 x86 架构的 Windows 上使用 Java 21 编译时,默认会抛出如下提示信息

bash ./configure
...
checking compilation type... native
configure: error: The Windows 32-bit x86 port is deprecated and may be removed in a future release. \
Use --enable-deprecated-ports=yes to suppress this error.
configure exiting with result code 1

可以使用 --enable-deprecated-ports=yes 继续编译

bash ./configure --enable-deprecated-ports=yes

幸运的是,历史 JDK 版本仍然支持 32 位 x86 架构 Windows 系统,不会受到影响

JEP 451 – 准备禁用代理的动态加载

简而言之,代理的动态加载时,发出警告

当代理在运行中的 JVM 中动态加载时,会发出警告。这些警告的目的是让用户为将来的版本做好准备,将来的版本默认不允许动态加载代理,以提高 Java 安全性

在任何版本中,Java 自带的工具在启动时加载代理不会发出警告,如 jcmdjconsole

JEP 452 – 密钥封装机制 API

特性介绍

简而言之,Java 提供了 KEM API

在计算机安全领域,KEM(Key Encapsulation Mechanism,密钥封装机制)是一种加密技术,它允许两个通信方安全地共享一个秘密密钥,而无需通过不安全的通道直接传输密钥。KEM 通常与对称加密算法结合使用,以实现高效的数据加密和安全通信

加密时,传统方法是使用公钥随机生成一个对称密钥,但这需要填充,也难以保证安全性。而密钥封装机制(KEM)则利用公钥的属性派生出一个相关的对称密钥,这种方法不需要填充

Java 平台传统的加密方式无法抵御量子攻击,而 KEM API 则可以有效弥补这个短板

KEM 的三个函数

KEM 算法由以下 3 个函数组成

  1. 密钥对生成函数

返回包含公钥和私钥的密钥对,由已有的 KeyPairGenerator API 提供
2. 密钥封装函数

sender 调用密钥封装函数,根据 receiver 的公钥和封装选项,生成密钥 K,以及一个封装消息,然后 sender 将这个封装消息发送给 receiver
3. 密钥解封装函数

receiver 接收到封装消息后,使用自己的私钥解密,得到密钥 K

KEM API

KEM API 相关的接口和类都位于 javax.crypto 包下

Java 21 新增了 KEMSpi(KEM Service Provider Interface),其中包括 EncapsulatorSpiDecapsulatorSpi。Java 提供的默认实现是 DHKEM,即 Diffie-Hellman KEM

public interface KEMSpi {
  EncapsulatorSpi engineNewEncapsulator(PublicKey var1, AlgorithmParameterSpec var2, SecureRandom var3) throws InvalidAlgorithmParameterException, InvalidKeyException;

  DecapsulatorSpi engineNewDecapsulator(PrivateKey var1, AlgorithmParameterSpec var2) throws InvalidAlgorithmParameterException, InvalidKeyException;

  public interface DecapsulatorSpi {
    SecretKey engineDecapsulate(byte[] var1, int var2, int var3, String var4) throws DecapsulateException;

    int engineSecretSize();

    int engineEncapsulationSize();
  }

  public interface EncapsulatorSpi {
    KEM.Encapsulated engineEncapsulate(int var1, int var2, String var3);

    int engineSecretSize();

    int engineEncapsulationSize();
  }
}

此外,还新增了 KEM

KEM.jpeg

小结

Java 21 是一个重要版本,尤其是分代 ZGC 和虚拟线程,其他语言层面的改进也值得关注。作为长期支持版本,值得在实际项目中逐步尝试升级,它将不负众望

参考文档

Oracle – The Arrival of Java 21

JEP 431 – Sequenced Collections

JEP 439 – Generational ZGC

JEP 440 – Record Patterns

JEP 441 – Pattern Matching for switch

JEP 444 – Virtual Threads

JEP 449 – Deprecate the 32-bit x86 Port for Removal

JEP 451 – To Disallow the Dynamic Loading of Agents

JEP 452 – Key Encapsulation Mechanism API

ZGC 官网

ZGC 日志理解

原文链接: https://juejin.cn/post/7383311950174339110

文章收集整理于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除,如若转载,请注明出处:http://www.cxyroad.com/17912.html

QR code