21是继Java17之后,最新的LTS版本,于2023年9月发布

正式特性

虚拟线程

该版本中虚拟线程成为了正式版,Java 19中是预览版

虚拟线程的实际应用

虚拟线程与传统线程池的对比

switch格式匹配

Java 14 版本推出了 Switch 表达式,能够一行处理多个条件;Java 21 版本进一步优化了 Switch 的能力,新增了模式匹配特性,能够更轻松地根据对象的类型做不同的处理。

之前的写法

新的写法,代码更加简洁

可以在switch中使用when

模式匹配的高级用法

record pattern

Java14引入的新特性。通过该特性可以解构record类型中的值,例如

或者

这种写法适合追求极致简洁代码的程序员,可以在一行代码中同时完成 类型检查、数据提取 和 条件判断。

Record 模式的实际应用

有序集合

在Java.util包下新增了3个接口

  1. SequencedCollection

  2. SequencedSet

  3. SequencedMap

为我们提供了更直观的方式来操作集合的头尾元素,说白了就是补了几个方法

除了 List 之外,SequencedMap 接囗(比如 LinkedHashMap) 和 SequencedSet 接口(比如 LinkedHashset)也新增了类似的方法。本质上都是实现了有序集合接口

示例:

分代 ZGC

Java 21 中的分代 ZGC 可以说是垃圾收集器领域的一个重大突破。ZGC 从 Java 11 开始就以其超低延迟而闻名,但是它并没有采用分代的设计思路

在这之前,ZGC对所有对象一视同仁,无论是刚创建的新对象还是存活了很久的老对象,都使用同样的收集策略。这虽然保证了一致的低延迟,但在内存分配密集的应用中,效率并不是最优的。 分代 ZGC 的核心思想是基于一个现象 —— 大部分对象都是"朝生夕死"的。它将堆内存划分为年轻代和老年代两个区域,年轻代的垃圾收集可以更加频繁和高效,因为大部分年轻对象很快就会死亡,收集器可以快速清理掉这些垃圾;而老年代的收集频率相对较低,减少了对长期存活对象的不必要扫描。 使用

分代 ZGC 的优势

  • 更低的分配开销:年轻代分配更高效

  • 更少的 GC 工作:大部分对象在年轻代就被回收

  • 更好的缓存局部性:年轻对象聚集在一起

  • 保持低延迟:继承了 ZGC的低延迟特性

弃用 Windows 32 位 x86 端囗

Java 21 将 Windows 32 位支持标记为弃用

准备禁止代理的动态加载

Java 21 为禁止运行时动态加载 Java 代理做准备

密钥封装机制 API

Java 21 引入了密钥封装机制(KEM)API

预览特性

字符串模板

字符串模板可以让开发者更简洁的进行字符串拼接(例如拼接sql,xml,json等)。该特性并不是为字符串拼接运算符+提供的语法糖,也并非为了替换SpringBuffer和StringBuilder。

利用STR模板进行字符串与变量的拼接:

这个特性目前是预览版,编译和运行需要添加额外的参数:

在js中字符串进行拼接时会采用下面的字符串插值写法

看起来字符串插值写法更简洁移动,不过若在Java中使用这种字符串插值的写法拼接sql,可能会出现sql注入的问题,为了防止该问题,Java提供了字符串模板表达式的方式。

上面使用的STR是Java中定义的模板处理器,它可以将变量的值取出,完成字符串的拼接。在每个Java源文件中都引入了一个public static final修饰的STR属性,因此我们可以直接使用STR,STR通过打印STR可以知道它是Java.lang.StringTemplate,是一个接口。

在StringTemplate中是通过调用interpolate方法来执行的,该方法分别传入了两个参数:

  • fragements:包含字符串模板中所有的字面量,是一个List

  • values:包含字符串模板中所有的变量,是一个List

而该方法又调用了JavaTemplateAccess中的interpolate方法,经过分析可以得知,它最终是通过String中的join方法将字面量和变量进行的拼接

其他使用示例,在STR中可以进行基本的运算(支持三元运算)

调用方法:

获取属性:

查看时间:

计数操作:

获取数组数据:

拼接多行数据:

自己定义字符串模板,通过StringTemplate来自定义模板

未命名模式和变量(预览)

Java 21 引入了未命名模式和变量

未命名类和实例主方法(预览)

Java 21 简化了简单程序的编写

scoped values(第一次预览)

ThreadLocal的问题

在 Web 应用中,一个请求通常会被多个线程处理,每个线程需要访问自己的数据,使用 ThreadLocal 可以确保数据在每个线程中的独立性。但由于ThreadLocal在设计上的瑕疵,导致下面问题:

  1. 内存泄漏:在用完ThreadLocal之后若没有调用remove,这样就会出现内存泄漏。

  2. 增加开销:在具有继承关系的线程中,子线程需要为父线程中ThreadLocal里面的数据分配内存。

  3. 权限问题:任何可以调用ThreadLocal中get方法的代码都可以随时调用set方法,这样就不易辨别哪些方法是按照什么顺序来更新的共享数据,并且这些方法也都有权限给ThreadLocal赋值。

随着虚拟线程的到来,内存泄漏问题就不用担心了,由于虚拟线程会很快的终止,此时会自动删除ThreadLocal中的数据,这样就不用调用remove方法了。但虚拟线程的数量通常是多的,试想下上百万个虚拟线程都要拷贝一份ThreadLocal中的变量,这会使内存承受更大的压力。为了解决这些问题,scoped values就出现了。scoped values 是一个隐藏的方法参数,只有方法可以访问scoped values,它可以让两个方法之间传递参数时无需声明形参。

ScopeValue初体验

基本用法

ScopedValue对象用jdk.incubator.concurrent包中的ScopedValue类来表示。使用ScopedValue的第一步是创建ScopedValue对象,通过静态方法newInstance来完成,ScopedValue对象一般声明为static final,每个线程都能访问自己的scope value,与ThreadLocal不同的是,它只会被write 1次且仅在线程绑定的期间内有效。

下一步是指定ScopedValue对象的值和作用域,通过静态方法where来完成。where方法有 3 个参数:

  • ScopedValue 对象

  • ScopedValue 对象所绑定的值

  • RunnableCallable对象,表示ScopedValue对象的作用域

RunnableCallable对象执行过程中,其中的代码可以用ScopedValue对象的get方法获取到where方法调用时绑定的值。这个作用域是动态的,取决于RunnableCallable对象所调用的方法,以及这些方法所调用的其他方法。当RunnableCallable对象执行完成之后,ScopedValue对象会失去绑定,不能再通过get方法获取值。在当前作用域中,ScopedValue对象的值是不可变的,除非再次调用where方法绑定新的值。这个时候会创建一个嵌套的作用域,新的值仅在嵌套的作用域中有效。使用作用域值有以下几个优势:

  • 提高数据安全性:由于作用域值只能在当前范围内访问,因此可以避免数据泄露或被恶意修改。

  • 提高数据效率:由于作用域值是不可变的,并且可以在线程之间共享,因此可以减少数据复制或同步的开销。

  • 提高代码清晰度:由于作用域值只能在当前范围内访问,因此可以减少参数传递或全局变量的使用。

下面代码模拟了送礼和收礼的场景

多线程操作相同的ScopeValue

不同的线程在操作同一个ScopeValue时,相互间不会影响,其本质是利用了Thread类中scopedValueBindings属性进行的线程绑定。

ScopeValue的修改

通过上面的示例可以看到,ScopeValue的值是在第一次使用where的时候就设置好了,该值在当前线程使用的期间是不会被修改的,这样就提高了性能。当然,我们也可以修改ScopeValue中的值,但需要注意,这里的修改会不影响本次方法中读取的值,而是会导致where后run中调用的方法里面的值发生变化。

所以,从以上分析可以看到,ScopedValue有一定的权限控制:就算在同一个线程中也不能任意修改ScopedValue的值,就算修改了对当前作用域(方法)也是无效的。另外ScopedValue也不需要手动remove,关于这块就需要分析它的实现原理了。这块内容待更新~

Structured Concurrency

该特性主要作用是在使用虚拟线程时,可以使任务和子任务的代码编写起来可读性更强,维护性更高,更加可靠。

孵化器特性

向量 API(第六次孵化器)

Java 21 继续完善向量 API