Java22有你需要的新特性嗎?
從 2017 年開始,Java 版本更新策略從原來的每兩年一個新版本,改為每六個月一個新版本,以快速驗證新特性,推動 Java 的發(fā)展。讓我們跟隨 Java 的腳步,配合示例講解,看一看每個版本的新特性,本期是 Java22 的新特性。
概述
Java22 在 2024 年 3 月 19 日發(fā)布GA版本,共十二大特性:
- JEP 423: G1垃圾回收器的區(qū)域固定(Region Pinning for G1)
- JEP 447: super(...)前置語句(Statements before super(...),預覽)
- JEP 454: 外部函數(shù)和內(nèi)存API(Foreign Function & Memory API)
- JEP 456: 未命名變量和模式(Unnamed Variables & Patterns)
- JEP 457: 類文件API(Class-File API,預覽)
- JEP 458: 啟動多文件源代碼程序(Launch Multi-File Source-Code Programs)
- JEP 459: 字符串模板(String Templates,第二次預覽)
- JEP 460: 向量API(Vector API,第七次孵化)
- JEP 461: 流收集器(Stream Gatherers,預覽)
- JEP 462: 結(jié)構(gòu)化并發(fā)(Structured Concurrency,第二次預覽)
- JEP 463: 隱式聲明的類和實例主方法(Implicitly Declared Classes and Instance Main Methods,第二次預覽)
- JEP 464: 作用域值(Scoped Values,第二次預覽)
接下來我們一起看看這些特性。
JEP 423: G1垃圾回收器的區(qū)域固定(Region Pinning for G1)
JEP 423: G1垃圾回收器的區(qū)域固定旨在解決在使用Java本地接口(JNI)時遇到的垃圾回收(GC)延遲問題。
在使用JNI時,Java線程需要等待GC操作完成,這會導致應用程序的延遲增加。特別是在JNI關鍵區(qū)域,GC操作會被暫停,直到線程離開該區(qū)域。這種機制雖然可以確保GC的穩(wěn)定性,但會顯著增加應用程序的延遲。
JEP 423通過引入?yún)^(qū)域固定機制來解決上述問題。具體來說,該特性允許在G1垃圾回收器中固定JNI代碼使用的內(nèi)存區(qū)域,這樣即使在這些區(qū)域中存在GC操作,也不會影響到其他區(qū)域的垃圾回收。這通過以下方式實現(xiàn):
- 區(qū)域計數(shù)器:在每個區(qū)域中維護一個計數(shù)器,用于記錄該區(qū)域中的臨界對象數(shù)量。當一個臨界對象被獲取時,計數(shù)器增加;當一個臨界對象被釋放時,計數(shù)器減少。
- 區(qū)域固定:在進行GC操作時,G1垃圾回收器會固定那些包含臨界對象的區(qū)域,確保這些區(qū)域在GC期間保持不變。這樣,即使線程處于JNI關鍵區(qū)域,垃圾回收也可以繼續(xù)進行,而不會被暫停。
JEP 423可以帶來顯著的性能改進:
- 減少延遲:通過允許在JNI關鍵區(qū)域期間繼續(xù)進行垃圾回收,減少了應用程序的延遲。
- 提高效率:Java線程無需等待GC操作完成,從而提高了開發(fā)人員的工作效率。
- 增強可預測性:該特性還增強了垃圾回收的可預測性,特別是在處理大對象時。
JEP 454: 外部函數(shù)和內(nèi)存API(Foreign Function & Memory API)
FFM API是為了提供一個更安全、更高效的替代JNI(Java Native Interface)的API。JNI雖然允許Java程序調(diào)用本地代碼,但其使用復雜且容易引入安全問題。FFM API旨在簡化這一過程,提高開發(fā)者的生產(chǎn)力和體驗,同時增強性能、安全性和一致性。在Java22中正式發(fā)布。
FFM API通過有效地調(diào)用外部函數(shù)(即JVM外部的代碼)并安全地訪問外部內(nèi)存(即不受JVM管理的內(nèi)存),使Java程序能夠調(diào)用本機庫并處理本機數(shù)據(jù),而不會出現(xiàn)脆弱性和危險。
FFM API經(jīng)歷了多輪孵化和預覽,從Java17的JEP 412開始,經(jīng)過Java18的JEP 419和Java19的JEP 424,再到Java20的JEP 434和Java21的JEP 442,最終在Java22中正式發(fā)布。這些改進包括:
- API的集中管理:通過Arena接口集中管理本地段的生命周期。
- 安全訪問外部內(nèi)存:通過MemoryLayout和VarHandle操作和訪問結(jié)構(gòu)化的外部內(nèi)存。
- 調(diào)用外部函數(shù):通過Linker、FunctionDescriptor和SymbolLookup調(diào)用外部函數(shù)。
FFM API的主要收益包括:
- 提高性能:通過直接調(diào)用本地函數(shù)和操作內(nèi)存,提高了程序的執(zhí)行效率。
- 增強安全性:通過更嚴格的內(nèi)存管理機制,減少了內(nèi)存泄漏和安全漏洞的風險。
- 提升開發(fā)體驗:簡化了與本地代碼的交互,使得開發(fā)者可以更專注于業(yè)務邏輯的實現(xiàn)。
我們看下官方示例:
// 1. 在C庫路徑上查找名為radixsort的外部函數(shù)
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
final MemorySegment memorySegment = stdlib.find("radixsort").orElseThrow();
FunctionDescriptor descriptor = FunctionDescriptor.ofVoid(
ValueLayout.ADDRESS,
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS
);
MethodHandle radixsort = linker.downcallHandle(memorySegment, descriptor);
// 下面的代碼將使用這個外部函數(shù)對字符串進行排序
// 2. 分配棧上內(nèi)存來存儲四個字符串
String[] javaStrings = {"mouse", "cat", "dog", "car"};
// 3. 使用try-with-resources來管理離堆內(nèi)存的生命周期
try (Arena offHeap = Arena.ofConfined()) {
// 4. 分配一段離堆內(nèi)存來存儲四個指針
MemorySegment pointers = offHeap.allocateArray(ValueLayout.ADDRESS, javaStrings.length);
// 5. 將字符串從棧上內(nèi)存復制到離堆內(nèi)存
for (int i = 0; i < javaStrings.length; i++) {
MemorySegment cString = offHeap.allocateUtf8String(javaStrings[i]);
pointers.setAtIndex(ValueLayout.ADDRESS, i, cString);
}
// 6. 通過調(diào)用外部函數(shù)對離堆數(shù)據(jù)進行排序
radixsort.invoke(pointers, javaStrings.length, MemorySegment.NULL, '\0');
// 7. 將排序后的字符串從離堆內(nèi)存復制回棧上內(nèi)存
for (int i = 0; i < javaStrings.length; i++) {
MemorySegment cString = pointers.getAtIndex(ValueLayout.ADDRESS, i);
javaStrings[i] = cString.getUtf8String(0);
}
} // 8. 所有離堆內(nèi)存在此處被釋放
// 驗證排序結(jié)果
assert Arrays.equals(javaStrings, new String[] {"car", "cat", "dog", "mouse"}); // true
我們都知道,JNI也是可以調(diào)用外部代碼的,那FFM API相較于JNI的優(yōu)勢在于:
- 更安全的內(nèi)存訪問:FFM API 提供了一種更安全和受控的方式來與本地代碼交互,避免了JNI中常見的內(nèi)存泄漏和數(shù)據(jù)損壞問題。
- 直接訪問本地內(nèi)存:FFM API 允許Java程序直接訪問本地內(nèi)存(即Java堆外的內(nèi)存),這使得數(shù)據(jù)處理更加高效和靈活。
- 跨語言函數(shù)調(diào)用:FFM API 支持調(diào)用Java程序的外部函數(shù),以與外部代碼和數(shù)據(jù)一起操作,而無需依賴JNI的復雜機制。
- 更高效的集成:FFM API 使得Java與C、C++等語言編寫的庫集成更加方便和高效,特別是在數(shù)據(jù)處理和機器學習等領域。
- 減少代碼復雜性:FFM API 提供了一種更簡潔的API,減少了JNI中復雜的代碼編寫和維護工作。
- 更廣泛的適用性:FFM API 不僅適用于簡單的函數(shù)調(diào)用,還可以處理復雜的內(nèi)存管理任務,如堆外內(nèi)存的管理。
- 提高性能:FFM API 通過高效的調(diào)用外部函數(shù)和安全地訪問外部內(nèi)存,提高了程序的運行效率。
JEP 456: 未命名變量和模式(Unnamed Variables & Patterns)
JEP 456: 未命名變量和模式(Unnamed Variables & Patterns)是一個重要新特性,在Java21中預覽,在Java22中發(fā)布。旨在提高Java代碼的可讀性和可維護性。這一特性允許開發(fā)者在聲明變量或嵌套模式時使用下劃線字符(_)來表示未命名的變量或模式,從而簡化代碼并減少不必要的噪聲,提高代碼可讀性和可維護性。
比如:
public static void main(String[] args) {
var _ = new Point(1, 2);
}
record Point(int x, int y) {
}
這個可以用在任何定義變量的地方,比如:
- ... instanceof Point(_, int y)
- r instanceof Point _
- switch …… case Box(_)
- for (Order _ : orders)
- for (int i = 0, _ = sideEffect(); i < 10; i++)
- try { ... } catch (Exception _) { ... } catch (Throwable _) { ... }
只要是這個不準備用,可以一律使用_代替。
JEP 458: 啟動多文件源代碼程序(Launch Multi-File Source-Code Programs)
這個功能主要是提升java命令的能力。比如我們手搓了兩個類:
// Prog.java
class Prog {
public static void main(String[] args) { Helper.run(); }
}
// Helper.java
class Helper {
static void run() { System.out.println("Hello!"); }
}
想要運行Prog的main方法,在JEP 458之前,我們需要先編譯Helper,然后通過-classpath指令加載編譯后的類,才能運行Prog。但是現(xiàn)在,java幫我們做了,我們直接使用java Prog.java就可以執(zhí)行了。
很方便的一個功能,但是似乎好像大概看起來沒什么用,但是對于Java生態(tài)有深遠影響:
- 增強Java啟動器功能:JEP 458允許Java啟動器執(zhí)行包含一個或多個文件的Java源碼應用程序。這一改進使得開發(fā)者可以更靈活地啟動和運行Java程序,特別是在需要處理多個源文件的復雜項目中,這一特性將大大提升開發(fā)效率和便利性。
- 促進Java生態(tài)系統(tǒng)的持續(xù)演進:JEP 458的引入是Java生態(tài)系統(tǒng)持續(xù)演進的一部分,與Java21的新特性和Quarkus的創(chuàng)新應用相輔相成。這些更新不僅提升了與現(xiàn)代Java生態(tài)系統(tǒng)的兼容性,還引入了領域模型驗證的改進和依賴于Java17的新特性,為開發(fā)者提供了更加穩(wěn)定、高效的工具集。
- 推動Java項目的發(fā)展:JEP 458的新特性被歸類到四個主要的Java項目中,即Amber、Loom、Panama和Valhalla。這些項目旨在通過精巧的合并,孵化一系列組件,以便最終將其納入到JDK中。這表明JEP 458不僅是一個獨立的特性,而是Java生態(tài)系統(tǒng)中更廣泛技術(shù)演進的一部分,有助于推動整個Java生態(tài)系統(tǒng)的創(chuàng)新和發(fā)展。
- 簡化Java開發(fā)流程:通過簡化啟動多文件源碼程序的過程,JEP 458有助于簡化Java開發(fā)流程,使得開發(fā)者可以更加專注于業(yè)務邏輯的實現(xiàn),而不是在啟動和運行程序上花費過多時間。
預覽功能
JEP 447: super(...)前置語句(Statements before super(...))
我們都知道,在子類的構(gòu)造函數(shù)中,如果通過super(……)調(diào)用父類,在super之前是不允許有其他語句的。
大部分的時候這種限制都沒問題,但是有時候不太靈活。如果想在super之前加上一些子類特有邏輯,比如想統(tǒng)計下子類構(gòu)造耗時,就得重寫一遍父類的實現(xiàn)。
除了有損靈活性,這種重寫的做法也會造成父子類之間的關系變得奇怪。假設父類是SDK中的一個類,SDK升級時在父類構(gòu)造函數(shù)增加了一些邏輯,我們項目中是無法繼承這些邏輯的,某次需要升級SDK(比如低版本有安全風險),驗證不完整的情況下,就很容易出現(xiàn)bug。
在 JEP 447 中,允許在構(gòu)造函數(shù)中不引用正在創(chuàng)建的實例的語句出現(xiàn)在顯式構(gòu)造函數(shù)調(diào)用(如 super())之前。這一特性旨在為開發(fā)者提供更多的靈活性,允許在調(diào)用父類構(gòu)造函數(shù)之前執(zhí)行一些驗證或其他處理操作。
引入這一特性的主要動機是提高構(gòu)造函數(shù)的靈活性和可讀性。通過允許在 super() 調(diào)用之前執(zhí)行語句,開發(fā)者可以在構(gòu)造函數(shù)中添加額外的邏輯,例如進行一些初始化檢查或執(zhí)行一些特定的處理操作,而不必依賴于 super() 調(diào)用之后的代碼。
我們看下示例代碼:
public class PositiveBigInteger extends BigInteger {
public PositiveBigInteger(long value) {
if (value <= 0) {
throw new IllegalArgumentException("non-positive value");
}
super(value);
}
}
JEP 457: 類文件API(Class-File API,預覽)
Java中一直缺少官方的類文件操作API,想要操作class,我們需要借助第三方庫,比如javassist、ASM、ByteBuddy等。從2017年開始,Java每半年有一次升級,特性更新頻率增加,需要第三方庫同步更新,是比較困難的。
還有一個原因是Java中使用了ASM實現(xiàn)jar、jlink等工具,以及l(fā)ambda表達式等。這就會出現(xiàn)一個問題,Java版本N依賴了ASM版本M,如果Java N中有類API,ASM M中是不會有的,只有Java N發(fā)布后,ASM升級到M+1才會有,Java想要使用ASM M+1,需要升級到Java N+1。是不是很顛,一個官方基礎語言,居然要依賴一個依賴這個語言的第三方工具。是可忍孰不可忍。
于是有了JEP 457,目標是提供一個準確、完整、高性能且遵循Java虛擬機規(guī)范定義的類文件格式的API,最終也會替換JDK內(nèi)部的ASM副本。
因為當前還是預覽版,我們先簡單看下官方示例:
如果要實現(xiàn)下面這段:
void fooBar(boolean z, int x) {
if (z)
foo(x);
else
bar(x);
}
我們在ASM的寫法:
ClassWriter classWriter = ...;
MethodVisitor mv = classWriter.visitMethod(0, "fooBar", "(ZI)V", null, null);
mv.visitCode();
mv.visitVarInsn(ILOAD, 1);
Label label1 = new Label();
mv.visitJumpInsn(IFEQ, label1);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 2);
mv.visitMethodInsn(INVOKEVIRTUAL, "Foo", "foo", "(I)V", false);
Label label2 = new Label();
mv.visitJumpInsn(GOTO, label2);
mv.visitLabel(label1);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 2);
mv.visitMethodInsn(INVOKEVIRTUAL, "Foo", "bar", "(I)V", false);
mv.visitLabel(label2);
mv.visitInsn(RETURN);
mv.visitEnd();
在JEP 457中的寫法:
ClassBuilder classBuilder = ...;
classBuilder.withMethod("fooBar", MethodTypeDesc.of(CD_void, CD_boolean, CD_int), flags,
methodBuilder -> methodBuilder.withCode(codeBuilder -> {
Label label1 = codeBuilder.newLabel();
Label label2 = codeBuilder.newLabel();
codeBuilder.iload(1)
.ifeq(label1)
.aload(0)
.iload(2)
.invokevirtual(ClassDesc.of("Foo"), "foo", MethodTypeDesc.of(CD_void, CD_int))
.goto_(label2)
.labelBinding(label1)
.aload(0)
.iload(2)
.invokevirtual(ClassDesc.of("Foo"), "bar", MethodTypeDesc.of(CD_void, CD_int))
.labelBinding(label2);
.return_();
});
還可以這樣寫:
CodeBuilder classBuilder = ...;
classBuilder.withMethod("fooBar", MethodTypeDesc.of(CD_void, CD_boolean, CD_int), flags,
methodBuilder -> methodBuilder.withCode(codeBuilder -> {
codeBuilder.iload(codeBuilder.parameterSlot(0))
.ifThenElse(
b1 -> b1.aload(codeBuilder.receiverSlot())
.iload(codeBuilder.parameterSlot(1))
.invokevirtual(ClassDesc.of("Foo"), "foo",
MethodTypeDesc.of(CD_void, CD_int)),
b2 -> b2.aload(codeBuilder.receiverSlot())
.iload(codeBuilder.parameterSlot(1))
.invokevirtual(ClassDesc.of("Foo"), "bar",
MethodTypeDesc.of(CD_void, CD_int))
.return_();
});
寫法上比ASM更加優(yōu)雅。
JEP 459: 字符串模板(String Templates,第二次預覽)
字符串模板是一個值得期待的功能,旨在增強Java編程語言。該特性通過將文字文本與嵌入式表達式和模板處理器結(jié)合,生成專門的結(jié)果,從而補充了Java現(xiàn)有的字符串文字和文本塊。
相對Java21中JEP 430,主要的優(yōu)化改動包括:
- 增強的表達式支持:允許在字符串模板中嵌入更復雜的表達式,這些表達式可以在運行時被計算和校驗。
- 模板處理器:引入了模板處理器的概念,使得字符串模板可以更加靈活地處理不同的數(shù)據(jù)格式和結(jié)構(gòu)。
- 安全性提升:通過在運行時對嵌入的表達式進行校驗,提高了代碼的安全性。
字符串模板通過將文本和嵌入式表達式結(jié)合在一起,使得Java程序能夠以一種更加直觀和安全的方式構(gòu)建字符串。與傳統(tǒng)的字符串拼接(使用+操作符)、StringBuilder或String.format 等方法相比,字符串模板提供了一種更加清晰和安全的字符串構(gòu)建方式。特別是當字符串需要從用戶提供的值構(gòu)建并傳遞給其他系統(tǒng)時(例如,構(gòu)建數(shù)據(jù)庫查詢),使用字符串模板可以有效地驗證和轉(zhuǎn)換模板及其嵌入表達式的值,從而提高Java程序的安全性。
讓我們通過代碼看一下這個特性的魅力:
public static void main(String[] args) {
// 拼裝變量
String name = "看山";
String info = STR. "My name is \{ name }" ;
assert info.equals("My name is 看山");
// 拼裝變量
String firstName = "Howard";
String lastName = "Liu";
String fullName = STR. "\{ firstName } \{ lastName }" ;
assert fullName.equals("Howard Liu");
String sortName = STR. "\{ lastName }, \{ firstName }" ;
assert sortName.equals("Liu, Howard");
// 模板中調(diào)用方法
String s2 = STR. "You have a \{ getOfferType() } waiting for you!" ;
assert s2.equals("You have a gift waiting for you!");
Request req = new Request("2017-07-19", "09:15", "https://www.howardliu.cn");
// 模板中引用對象屬性
String s3 = STR. "Access at \{ req.date } \{ req.time } from \{ req.address }" ;
assert s3.equals("Access at 2017-07-19 09:15 from https://www.howardliu.cn");
LocalTime now = LocalTime.now();
String markTime = DateTimeFormatter
.ofPattern("HH:mm:ss")
.format(now);
// 模板中調(diào)用方法
String time = STR. "The time is \{
// The java.time.format package is very useful
DateTimeFormatter
.ofPattern("HH:mm:ss")
.format(now)
} right now" ;
assert time.equals("The time is " + markTime + " right now");
// 模板嵌套模板
String[] fruit = {"apples", "oranges", "peaches"};
String s4 = STR. "\{ fruit[0] }, \{
STR. "\{ fruit[1] }, \{ fruit[2] }"
}" ;
assert s4.equals("apples, oranges, peaches");
// 模板與文本塊結(jié)合
String title = "My Web Page";
String text = "Hello, world";
String html = STR. """
<html>
<head>
<title>\{ title }</title>
</head>
<body>
<p>\{ text }</p>
</body>
</html>
""" ;
assert html.equals("""
<html>
<head>
<title>My Web Page</title>
</head>
<body>
<p>Hello, world</p>
</body>
</html>
""");
// 帶格式化的字符串模板
record Rectangle(String name, double width, double height) {
double area() {
return width * height;
}
}
Rectangle[] zone = new Rectangle[] {
new Rectangle("Alfa", 17.8, 31.4),
new Rectangle("Bravo", 9.6, 12.4),
new Rectangle("Charlie", 7.1, 11.23),
};
String table = FMT. """
Description Width Height Area
%-12s\{ zone[0].name } %7.2f\{ zone[0].width } %7.2f\{ zone[0].height } %7.2f\{ zone[0].area() }
%-12s\{ zone[1].name } %7.2f\{ zone[1].width } %7.2f\{ zone[1].height } %7.2f\{ zone[1].area() }
%-12s\{ zone[2].name } %7.2f\{ zone[2].width } %7.2f\{ zone[2].height } %7.2f\{ zone[2].area() }
\{ " ".repeat(28) } Total %7.2f\{ zone[0].area() + zone[1].area() + zone[2].area() }
""";
assert table.equals("""
Description Width Height Area
Alfa 17.80 31.40 558.92
Bravo 9.60 12.40 119.04
Charlie 7.10 11.23 79.73
Total 757.69
""");
}
public static String getOfferType() {
return "gift";
}
record Request(String date, String time, String address) {
}
這個功能當前是第二次預覽,Java23的8.12版本中還沒有展示字符串模板的第三次預覽(JEP 465: String Templates),還不能確定什么時候可以正式用上。
JEP 461: 流收集器(Stream Gatherers,預覽)
JEP 461旨在增強Java Stream API、以支持自定義中間操作。這一特性允許開發(fā)者以更靈活和高效的方式處理數(shù)據(jù)流,從而提高流管道的表達能力和轉(zhuǎn)換數(shù)據(jù)的能力。
流收集器通過引入新的中間操作Stream::gather(Gatherer),允許開發(fā)者定義自定義的轉(zhuǎn)換實體(稱為Gatherer),從而對流中的元素進行轉(zhuǎn)換。這些轉(zhuǎn)換可以是一對一、一對多、多對一或多對多的轉(zhuǎn)換方式。此外,流收集器還支持保存以前遇到的元素,以便進行進一步的處理。
我們通過實例感受下這一特性的魅力:
public record WindowFixed<TR>(int windowSize) implements Gatherer<TR, ArrayList<TR>, List<TR>> {
public static void main(String[] args) {
var list = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.gather(new WindowFixed<>(3))
.toList();
// [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
System.out.println(list);
}
public WindowFixed {
// Validate input
if (windowSize < 1) {
throw new IllegalArgumentException("window size must be positive");
}
}
@Override
public Supplier<ArrayList<TR>> initializer() {
// 創(chuàng)建一個 ArrayList 來保存當前打開的窗口
return () -> new ArrayList<>(windowSize);
}
@Override
public Integrator<ArrayList<TR>, TR, List<TR>> integrator() {
// 集成器在每次消費元素時被調(diào)用
return Gatherer.Integrator.ofGreedy((window, element, downstream) -> {
// 將元素添加到當前打開的窗口
window.add(element);
// 直到達到所需的窗口大小,
// 返回 true 表示希望繼續(xù)接收更多元素
if (window.size() < windowSize) {
return true;
}
// 當窗口已滿時,通過創(chuàng)建副本關閉窗口
var result = new ArrayList<TR>(window);
// 清空窗口以便開始新的窗口
window.clear();
// 將關閉的窗口發(fā)送到下游
return downstream.push(result);
});
}
// 由于此操作本質(zhì)上是順序的,因此無法并行化,因此省略了合并器
@Override
public BiConsumer<ArrayList<TR>, Downstream<? super List<TR>>> finisher() {
// 終結(jié)器在沒有更多元素傳遞時運行
return (window, downstream) -> {
// 如果下游仍然接受更多元素且當前打開的窗口非空,則將其副本發(fā)送到下游
if (!downstream.isRejecting() && !window.isEmpty()) {
downstream.push(new ArrayList<TR>(window));
window.clear();
}
};
}
}
該特性還是預覽版,等正式發(fā)布后再細說。
JEP 462: 結(jié)構(gòu)化并發(fā)(Structured Concurrency,第二次預覽)
結(jié)構(gòu)化并發(fā)API(Structured Concurrency API)旨在簡化多線程編程,通過引入一個API來處理在不同線程中運行的多個任務作為一個單一工作單元,從而簡化錯誤處理和取消操作,提高可靠性,并增強可觀測性。本次發(fā)布是第一次預覽。
結(jié)構(gòu)化并發(fā)API提供了明確的語法結(jié)構(gòu)來定義子任務的生命周期,并啟用一個運行時表示線程間的層次結(jié)構(gòu)。這有助于實現(xiàn)錯誤傳播和取消以及并發(fā)程序的有意義觀察。
在JEP 462中,結(jié)構(gòu)化并發(fā)API被進一步完善,使其更加易于使用和維護。主要的優(yōu)化包括:
- 工作單元概念:將一組相關任務視為一個工作單元,這樣可以簡化錯誤處理和取消操作。
- 增強的可觀測性:通過API提供了更好的錯誤處理機制和任務狀態(tài)跟蹤功能,增強了代碼的可觀察性。
- 虛擬線程支持:與Project Loom項目結(jié)合,支持在線程內(nèi)和線程間共享不可變數(shù)據(jù),這在使用大量虛擬線程時尤其有用。
Java使用異常處理機制來管理運行時錯誤和其他異常。當異常在代碼中產(chǎn)生時,如何被傳遞和處理的過程稱為異常傳播。
在結(jié)構(gòu)化并發(fā)環(huán)境中,異常可以通過顯式地從當前環(huán)境中拋出并傳播到更大的環(huán)境中去處理。
在Java并發(fā)編程中,非受檢異常的處理是程序健壯性的重要組成部分。特別是對于非受檢異常的處理,這關系到程序在遇到錯誤時是否能夠優(yōu)雅地繼續(xù)運行或者至少提供有意義的反饋。
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var task1 = scope.fork(() -> {
Thread.sleep(1000);
return "Result from task 1";
});
var task2 = scope.fork(() -> {
Thread.sleep(2000);
return "Result from task 2";
});
scope.join();
scope.throwIfFailed(RuntimeException::new);
System.out.println(task1.get());
System.out.println(task2.get());
} catch (Exception e) {
e.printStackTrace();
}
在這個例子中,handle()方法使用StructuredTaskScope來并行執(zhí)行兩個子任務:task1和task2。通過使用try-with-resources語句自動管理資源,并確保所有子任務都在try塊結(jié)束時正確完成或被取消。這種方式使得線程的生命周期和任務的邏輯結(jié)構(gòu)緊密相關,提高了代碼的清晰度和錯誤處理的效率。使用 StructuredTaskScope 可以確保一些有價值的屬性:
- 錯誤處理與短路:如果task1或task2子任務中的任何一個失敗,另一個如果尚未完成則會被取消。(這由 ShutdownOnFailure 實現(xiàn)的關閉策略來管理;還有其他策略可能)。
- 取消傳播:如果在運行上面方法的線程在調(diào)用 join() 之前或之中被中斷,則線程在退出作用域時會自動取消兩個子任務。
- 清晰性:設置子任務,等待它們完成或被取消,然后決定是成功(并處理已經(jīng)完成的子任務的結(jié)果)還是失?。ㄗ尤蝿找呀?jīng)完成,因此沒有更多需要清理的)。
- 可觀察性:線程轉(zhuǎn)儲清楚地顯示了任務層次結(jié)構(gòu),其中運行task1或task2的線程被顯示為作用域的子任務。
上面的示例能夠很好的解決我們的一個痛點,有兩個可并行的任務A和B,A+B才是完整結(jié)果,任何一個失敗,另外一個也不需要成功,結(jié)構(gòu)化并發(fā)API就可以很容易的實現(xiàn)這個邏輯。
JEP 463: 隱式聲明的類和實例主方法(Implicitly Declared Classes and Instance Main Methods,第二次預覽)
無論學習哪門語言,第一課一定是打印Hello, World!,Java中的寫法是:
public class HelloWorld {
public static void main(String[] args) {
System.out.println ("Hello, World!");
}
}
如果是第一次接觸,一定會有很多疑問,public干啥的,main方法的約定參數(shù)args是什么鬼?然后老師就說,這就是模板,照著抄就行,不這樣寫不運行。
JEP 445特性后,可以簡化為:
class HelloWorld {
void main() {
System.out.println ("Hello, World!");
}
}
我們還可以這樣寫:
String greeting() { return "Hello, World!"; }
void main() {
System.out.println(greeting());
}
main方法直接簡化為名字和括號,甚至連類也不需要顯性定義了。雖然看起來沒啥用,但是在JShell中使用,就比較友好了。
JEP 464: 作用域值(Scoped Values,第二次預覽)
作用域值(Scoped Values)在Java20孵化,在Java21第一次預覽,在Java22第二次預覽,旨在提供一種安全且高效的方法來共享數(shù)據(jù),無需使用方法參數(shù)。這一特性允許在不使用方法參數(shù)的情況下,將數(shù)據(jù)安全地共享給方法,優(yōu)先于線程局部變量,特別是在使用大量虛擬線程時。
在多線程環(huán)境中,作用域值可以在線程內(nèi)和線程間共享不可變數(shù)據(jù),例如從父線程向子線程傳遞數(shù)據(jù),從而解決了在多線程應用中傳遞數(shù)據(jù)的問題。此外,作用域值提高了數(shù)據(jù)的安全性、不變性和封裝性,并且在多線程環(huán)境中使用事務、安全主體和其他形式的共享上下文的應用程序中表現(xiàn)尤為突出。
作用域值的主要特點:
- 不可變性:作用域值是不可變的,這意味著一旦設置,其值就不能更改。這種不可變性減少了并發(fā)編程中意外副作用的風險。
- 作用域生命周期:作用域值的生命周期僅限于 run 方法定義的作用域。一旦執(zhí)行離開該作用域,作用域值將不再可訪問。
- 繼承性:子線程會自動繼承父線程的作用域值,從而允許在線程邊界間無縫共享數(shù)據(jù)。
在這個功能之前,在多線程間傳遞數(shù)據(jù),我們有兩種選擇:
- 方法參數(shù):顯示參數(shù)傳遞;缺點是新增參數(shù)時修改聯(lián)動修改一系列方法,如果是框架或SDK層面的,無法做到向下兼容。
- ThreadLocal:在ThreadLocal保存當前線程變量。
使用過ThreadLocal的都清楚,ThreadLocal會有三大問題。
- 無約束的可變性:每個線程局部變量都是可變的。任何可以調(diào)用線程局部變量的get方法的代碼都可以隨時調(diào)用該變量的set方法。即使線程局部變量中的對象是不可變的,每個字段都被聲明為final,情況仍然如此。ThreadLocal API允許這樣做,以便支持一個完全通用的通信模型,在該模型中,數(shù)據(jù)可以在方法之間以任何方向流動。這可能會導致數(shù)據(jù)流混亂,導致程序難以分辨哪個方法更新共享狀態(tài)以及以何種順序進行。
- 無界生存期:一旦通過set方法設置了一個線程局部變量的副本,該值就會在該線程的生存期內(nèi)保留,或者直到該線程中的代碼調(diào)用remove方法。我們有時候會忘記調(diào)用remove,如果使用線程池,在一個任務中設置的線程局部變量的值如果不清除,可能會意外泄漏到無關的任務中,導致危險的安全漏洞(比如人員SSO)。對于依賴于線程局部變量的無約束可變性的程序來說,可能沒有明確的點可以保證線程調(diào)用remove是安全的,可能會導致內(nèi)存泄漏,因為每個線程的數(shù)據(jù)在退出之前都不會被垃圾回收。
- 昂貴的繼承:當使用大量線程時,線程局部變量的開銷可能會更糟糕,因為父線程的線程局部變量可以被子線程繼承。(事實上,線程局部變量并不是某個特定線程的本地變量。)當開發(fā)人員選擇創(chuàng)建一個繼承了線程局部變量的子線程時,該子線程必須為之前在父線程中寫入的每個線程局部變量分配存儲空間。這可能會顯著增加內(nèi)存占用。子線程不能共享父線程使用的存儲,因為ThreadLocal API要求更改線程的線程局部變量副本在其他線程中不可見。這也會有另一個隱藏的問題,子線程沒有辦法向父線程set數(shù)據(jù)。
作用域值可以有效解決上面提到的問題,而且寫起來更加優(yōu)雅。
我們一起看下作用域值的使用:
// 聲明一個作用域值用于存儲用戶名
public final static ScopedValue<String> USERNAME = ScopedValue.newInstance();
private static final Runnable printUsername = () ->
System.out.println(Thread.currentThread().threadId() + " 用戶名是 " + USERNAME.get());
public static void main(String[] args) throws Exception {
// 將用戶名 "Bob" 綁定到作用域并執(zhí)行 Runnable
ScopedValue.where(USERNAME, "Bob").run(() -> {
printUsername.run();
new Thread(printUsername).start();
});
// 將用戶名 "Chris" 綁定到另一個作用域并執(zhí)行 Runnable
ScopedValue.where(USERNAME, "Chris").run(() -> {
printUsername.run();
new Thread(() -> {
new Thread(printUsername).start();
printUsername.run();
}).start();
});
// 檢查在任何作用域外 USERNAME 是否被綁定
System.out.println("用戶名是否被綁定: " + USERNAME.isBound());
}
寫起來干凈利索,而且功能更強。
孵化功能
JEP 460: 向量API(Vector API,第七次孵化)
向量API的功能是提供一個表達向量計算的API,旨在通過引入向量計算API來提高Java應用程序的性能。這一API允許開發(fā)者在支持的CPU架構(gòu)上可靠地編譯為最佳向量指令,從而實現(xiàn)比等效的標量計算更高的性能。這些計算在運行時可靠地編譯成支持的CPU架構(gòu)上的最優(yōu)向量指令,從而實現(xiàn)比等效標量計算更優(yōu)的性能。
下面這個是官方給的示例:
// 標量計算示例
void scalarComputation(float[] a, float[] b, float[] c) {
for (int i = 0; i < a.length ; i++) {
c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
}
}
// 使用向量API的向量計算示例
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
void vectorComputation(float[] a, float[] b, float[] c) {
int i = 0;
int upperBound = SPECIES.loopBound(a.length);
for (; i < upperBound; i += SPECIES.length()) {
// FloatVector va, vb, vc;
var va = FloatVector.fromArray(SPECIES, a, i);
var vb = FloatVector.fromArray(SPECIES, b, i);
var vc = va.mul(va).add(vb.mul(vb)).neg();
vc.intoArray(c, i);
}
for (; i < a.length; i++) {
c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
}
}
向量API在Java中的獨特優(yōu)勢在于其高效的并行計算能力、豐富的向量化指令集、跨平臺的數(shù)據(jù)并行算法支持以及對機器學習的特別優(yōu)化。