likes
comments
collection
share

JDK 21 正式特性全面体验

作者站长头像
站长
· 阅读数 29

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

序言

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

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

新特性概览

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

JEP 编号特性中文名特性英文名特性阶段分类
440Record 模式Record Patterns正式Amber 项目
441switch 模式匹配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外部函数和内存 APIForeign Function & Memory API预览 3Panama 项目
448向量 APIVector API孵化 6Panama 项目
431有序集合接口Sequenced Collections正式Core
439分代 ZGCGenerational ZGC正式性能提升
452密钥封装机制 APIKey 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: 一台电脑上可以同时安装专业版和社区版) JDK 21 正式特性全面体验 在 IDEA 中新建一个工程 jdk-feature,选择已经下载好的 JDK21,Language Level 选择 21 JDK 21 正式特性全面体验

JEP 431 - 有序集合接口

特性介绍

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

在集合框架中新增了 SequencedCollection, SequencedSet, SequencedMap 接口 JDK 21 正式特性全面体验

在此之前,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 参数 JDK 21 正式特性全面体验

由于要观察 JVM 内存变化情况,可以下载 VisualVM,打开后依次点击菜单栏 Tools / Plugins 下载 Visual GC 插件 JDK 21 正式特性全面体验

以下代码构造了 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 分析出前一段时间的内存使用率较低时,主动降低了堆内存,之后再根据已用内存自动调整堆内存 JDK 21 正式特性全面体验 重要 GC 日志如下,算上预热,共有 66 次 GC,GC 总时间为 3215ms,其中新生代总时间为 1161ms,老年代总时间为 2054ms JDK 21 正式特性全面体验 JDK 21 正式特性全面体验 JDK 21 正式特性全面体验 JDK 21 正式特性全面体验 通过分析 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 分析出前一段时间的内存使用率较低时,主动降低了堆内存,之后再根据已用内存自动调整堆内存。可以看出,已用内存趋势图要平滑一些 JDK 21 正式特性全面体验 重要 GC 日志如下,算上预热,共有 62 次 GC JDK 21 正式特性全面体验 JDK 21 正式特性全面体验 同样地,从中摘取停顿时间信息,以最坏的情况考虑,每次 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,变化幅度较大 JDK 21 正式特性全面体验 Visual GC 统计信息如下,算上预热,共有 10 次 GC,GC 总时间为 191.213ms JDK 21 正式特性全面体验 最后一次 GC 日志,可以看出,停顿时间为 18.754ms,比 ZGC 差了不止一个量级 JDK 21 正式特性全面体验

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 语句
  4. 优化 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 JDK 21 正式特性全面体验 其中有一个 VirtualThread-unparker 线程较为特殊 JDK 21 正式特性全面体验

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

此外,还新增了 KEMJDK 21 正式特性全面体验

小结

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

参考文档

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
评论
请登录