21是继Java17之后,最新的LTS版本,于2023年9月发布
正式特性
虚拟线程
该版本中虚拟线程成为了正式版,Java 19中是预览版
虚拟线程的实际应用
虚拟线程与传统线程池的对比
switch格式匹配
Java 14 版本推出了 Switch 表达式,能够一行处理多个条件;Java 21 版本进一步优化了 Switch 的能力,新增了模式匹配特性,能够更轻松地根据对象的类型做不同的处理。
之前的写法
新的写法,代码更加简洁
可以在switch中使用when
模式匹配的高级用法
record pattern
Java14引入的新特性。通过该特性可以解构record类型中的值,例如
或者
这种写法适合追求极致简洁代码的程序员,可以在一行代码中同时完成 类型检查、数据提取 和 条件判断。
Record 模式的实际应用
有序集合
在Java.util包下新增了3个接口
SequencedCollection
SequencedSet
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在设计上的瑕疵,导致下面问题:
内存泄漏:在用完ThreadLocal之后若没有调用remove,这样就会出现内存泄漏。
增加开销:在具有继承关系的线程中,子线程需要为父线程中ThreadLocal里面的数据分配内存。
权限问题:任何可以调用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对象所绑定的值Runnable或Callable对象,表示ScopedValue对象的作用域
在Runnable或Callable对象执行过程中,其中的代码可以用ScopedValue对象的get方法获取到where方法调用时绑定的值。这个作用域是动态的,取决于Runnable或Callable对象所调用的方法,以及这些方法所调用的其他方法。当Runnable或Callable对象执行完成之后,ScopedValue对象会失去绑定,不能再通过get方法获取值。在当前作用域中,ScopedValue对象的值是不可变的,除非再次调用where方法绑定新的值。这个时候会创建一个嵌套的作用域,新的值仅在嵌套的作用域中有效。使用作用域值有以下几个优势:
提高数据安全性:由于作用域值只能在当前范围内访问,因此可以避免数据泄露或被恶意修改。
提高数据效率:由于作用域值是不可变的,并且可以在线程之间共享,因此可以减少数据复制或同步的开销。
提高代码清晰度:由于作用域值只能在当前范围内访问,因此可以减少参数传递或全局变量的使用。
下面代码模拟了送礼和收礼的场景
多线程操作相同的ScopeValue
不同的线程在操作同一个ScopeValue时,相互间不会影响,其本质是利用了Thread类中scopedValueBindings属性进行的线程绑定。
ScopeValue的修改
通过上面的示例可以看到,ScopeValue的值是在第一次使用where的时候就设置好了,该值在当前线程使用的期间是不会被修改的,这样就提高了性能。当然,我们也可以修改ScopeValue中的值,但需要注意,这里的修改会不影响本次方法中读取的值,而是会导致where后run中调用的方法里面的值发生变化。
所以,从以上分析可以看到,ScopedValue有一定的权限控制:就算在同一个线程中也不能任意修改ScopedValue的值,就算修改了对当前作用域(方法)也是无效的。另外ScopedValue也不需要手动remove,关于这块就需要分析它的实现原理了。这块内容待更新~
Structured Concurrency
该特性主要作用是在使用虚拟线程时,可以使任务和子任务的代码编写起来可读性更强,维护性更高,更加可靠。
孵化器特性
向量 API(第六次孵化器)
Java 21 继续完善向量 API