如果你仍然認為之前的JDK 17沒有太多改變,那么JDK 21需要引起你的注意。因為JDK 21引入了一種新型的并發編程模型。
目前在JAVA中的多線程并發編程是我們頭痛的另一部分。感覺學起來很困難,使用起來也很復雜。但是回頭看看使用其他語言的朋友,他們根本沒有這種麻煩,比如GoLang,使用起來非常順暢。
JDK 21在這個領域取得了巨大的改進,使Java并發編程變得更加簡單和順暢。更準確地說,這些改進在JDK 19或JDK 20中已經存在。
1*wtYzgJzJD8rGtysVoIW1cw.png
其中,虛擬線程、作用域值和結構化并發是多線程并發編程的一些功能。
一、虛擬線程
虛擬線程是基于協程的線程,類似于其他語言中的協程,但也有一些區別。
虛擬線程附加在主線程上。如果主線程被銷毀,虛擬線程將不再存在。
相似之處:
- 虛擬線程和協程都很輕量級,它們的創建和銷毀開銷比傳統操作系統線程要小。
- 虛擬線程和協程都可以通過掛起和恢復來在線程之間切換,從而避免了線程上下文切換的開銷。
- 虛擬線程和協程都可以以異步和非阻塞的方式處理任務,提高了應用程序的性能和響應能力。
不同之處:
- 虛擬線程是在JVM級別實現的,而協程是在語言級別實現的。因此,虛擬線程的實現可以與支持JVM的任何語言一起使用,而協程的實現需要特定的編程語言支持。
- 虛擬線程是協程的基于線程的實現,因此它們可以使用與線程相關的API,如ThreadLocal、Lock和Semaphore。協程不依賴于線程,通常需要特定的異步編程框架和API。
- 虛擬線程的調度由JVM管理,而協程的調度由編程語言或異步編程框架管理。因此,虛擬線程可以更好地與其他線程合作,而協程更適合處理異步任務。
總的來說,虛擬線程是一種新的線程類型,可以提高應用程序的性能和資源利用率,同時還可以使用傳統的與線程相關的API。虛擬線程與協程有許多相似之處,但也存在一些不同之處。
虛擬線程確實可以使多線程編程更加簡單和高效。與傳統的操作系統線程相比,創建和銷毀虛擬線程的開銷更小,線程上下文切換的開銷也更小,因此可以大大減少多線程編程中的資源消耗和性能瓶頸。
使用虛擬線程,開發人員可以像編寫傳統線程代碼一樣編寫代碼,而不必擔心線程的數量和調度,因為JVM會自動管理虛擬線程的數量和調度。此外,虛擬線程還支持傳統的與線程相關的API,如ThreadLocal、Lock和Semaphore,這使得開發人員更容易將傳統線程代碼遷移到虛擬線程中。
虛擬線程的引入使多線程編程更加高效、簡單和安全,允許開發人員更多關注業務邏輯,而不必過多關注底層線程管理。
二、結構化并發
結構化并發是一種旨在通過提供結構化且易于遵循的方法來簡化并發編程的編程范例。使用結構化并發,開發人員可以創建更容易理解和調試、不容易出現競態條件和其他與并發相關的錯誤的并發代碼。在結構化并發中,所有并發代碼都被結構化為稱為任務的明確定義的工作單元。任務以結構化的方式創建、執行和完成,任務的執行始終保證在其父任務完成之前完成。
結構化并發可以使多線程編程更加簡單和可靠。在傳統的多線程編程中,線程的啟動、執行和終止都是由開發人員手動管理的,因此容易出現線程泄漏、死鎖和不正確的異常處理等問題。
使用結構化并發,開發人員可以更自然地組織并發任務,使任務之間的依賴關系更清晰,代碼邏輯更簡潔。結構化并發還提供了一些異常處理機制,以更好地管理并發任務中的異常,避免由異常引起的程序崩潰或數據不一致。
此外,結構化并發還可以通過限制并發任務的數量和優先級來防止資源
競爭和饑餓現象。這些特性使得開發人員能夠更容易地實現高效且可靠的并發程序,而不必過多關注底層線程管理。
三、作用域值
作用域值是JDK 20中的一項功能,允許開發人員創建僅限于特定線程或任務的作用域值。作用域值類似于線程本地變量,但設計用于與虛擬線程和結構化并發一起使用。它們允許開發人員以結構化的方式在不同部分的應用程序之間傳遞上下文信息,例如用戶身份驗證或請求特定數據。
四、實踐
在繼續以下探索之前,您需要至少下載JDK 19或直接下載JDK 20。截止到2023年9月,JDK 20是官方發布的最高版本。如果使用JDK 19,您將無法體驗到Scoped Values功能。
1*GQ22_fxZ-eRKk85BBXuHWQ.png
或者直接下載JDK 21的早期訪問版本。
1*0hHWnZaMVfsKlVCLjMJUSg.png
如果您使用的是IDEA,則您的IDEA版本必須至少為2022.3或更高版本,否則不支持這樣的新JDK版本。
如果您使用的是JDK 19或JDK 20,您應該在項目設置中將語言級別設置為19或20。否則,在編譯時會提示您無法使用預覽版本功能。虛擬線程是預覽版本的功能。
1*6oGVASOHa2kRTtbZ--F7AQ.png
如果您使用的是JDK 21,請將語言級別設置為X - 實驗性功能。此外,由于JDK 21不是官方版本,您需要進入IDEA設置(請注意,這是IDEA設置,而不是項目設置),并手動將項目的目標字節碼版本更改為21。當前,最高選項為20,即JDK 20。將其設置為21后,您可以在JDK 21中使用這些功能。
1*8ltpmMzUzE4u5CAGgNvIOg.png
1. 虛擬線程
現在我們如何啟動線程?
首先,聲明一個線程類,實現Runnable接口,并實現run方法。
public class SimpleThread implements Runnable {
@Override
public void run() {
System.out.println("name:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
然后,您可以使用這個線程類并啟動線程。
Thread thread = new Thread(new SimpleThread());
thread.start();
擁有虛擬線程后,如何實現呢?
Thread.ofPlatform().name("thread-test").start(new SimpleThread());
以下是使用虛擬線程的幾種方式。
(1) 直接啟動虛擬線程
Thread thread = Thread.startVirtualThread(new SimpleThread());
(2) 使用ofVirtual(),構建器模式啟動虛擬線程,您可以設置線程名稱、優先級、異常處理和其他配置
Thread.ofVirtual()
.name("thread-test")
.start(new SimpleThread());
或者:
Thread thread = Thread.ofVirtual()
.name("thread-test")
.uncaughtExceptionHandler((t, e) -> {
System.out.println(t.getName() + e.getMessage());
})
.unstarted(new SimpleThread());
thread.start();
(3) 使用工廠創建線程
ThreadFactory factory = Thread.ofVirtual().factory();
Thread thread = factory.newThread(new SimpleThread());
thread.setName("thread-test");
thread.start();
(4) 使用Executors
ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
Future<?> submit = executorService.submit(new SimpleThread());
Object o = submit.get();
2. 結構化并發
想象以下情景。假設您有三個任務需要同時執行。只要任何一個任務完成并返回結果,就可以直接使用該結果,可以停止其他兩個任務。例如,一個天氣服務通過三個渠道獲取天氣情況,只要一個渠道返回即可。
在這種情況下,在Java 8下應該做什么,當然也是可以的。
List<Future<?>> futures = executor.invokeAll(tasks);
String result = executor.invokeAny(tasks);
使用ExecutorService的invokeAll和invokeAny方法實現,但會有一些額外的工作。在獲取第一個結果后,您需要手動關閉另一個線程。
在JDK 21中,可以使用結構化編程來實現。
ShutdownOnSuccess捕獲第一個結果并關閉任務范圍以中斷未完成的線程并喚醒調用線程。
一種情況是任何子任務的結果都可以直接使用,而無需等待其他未完成任務的結果。
它定義了獲取第一個結果或在所有子任務失敗時拋出異常的方法。
public static void mAIn(String[] args) throws IOException {
try (var scope = new StructuredTaskScope.ShutdownOnSuccess()) {
Future<String> res1 = scope.fork(() -> runTask(1));
Future<String> res2 = scope.fork(() -> runTask(2));
Future<String> res3 = scope.fork(() -> runTask(3));
scope.join();
System.out.println("scope:" + scope.result());
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
}
public static String runTask(int i) throws InterruptedException {
Thread.sleep(
1000);
long l = new Random().nextLong();
String s = String.valueOf(l);
System.out.println(i + "task:" + s);
return s;
}
ShutdownOnFailure
執行多個任務,只要有一個失敗(發生異常或引發其他活動異常),就停止其他未完成的任務,并使用scope.throwIfFailed來捕獲并拋出異常。
如果所有任務都正常,可以使用Feture.get()或*Feture.resultNow()來獲取結果。
public static void main(String[] args) throws IOException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> res1 = scope.fork(() -> runTaskWithException(1));
Future<String> res2 = scope.fork(() -> runTaskWithException(2));
Future<String> res3 = scope.fork(() -> runTaskWithException(3));
scope.join();
scope.throwIfFailed(Exception::new);
String s = res1.resultNow();
System.out.println(s);
String result = Stream.of(res1, res2, res3)
.map(Future::resultNow)
.collect(Collectors.joining());
System.out.println("result:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}
public static String runTaskWithException(int i) throws InterruptedException {
Thread.sleep(1000);
long l = new Random().nextLong(3);
if (l == 0) {
throw new InterruptedException();
}
String s = String.valueOf(l);
System.out.println(i + "task:" + s);
return s;
}
3. 作用域值
我們一定使用過ThreadLocal,它是線程本地變量,只要線程不銷毀,就可以隨時獲取ThreadLocal中的變量值。作用域值也可以在線程內的任何時候獲取變量,但它有一個作用域的概念,當超出作用域時將被銷毀。
public class ScopedValueExample {
final static ScopedValue<String> LoginUser = ScopedValue.newInstance();
public static void main(String[] args) throws InterruptedException {
ScopedValue.where(LoginUser, "Tom")
.run(() -> {
new Service().login();
});
Thread.sleep(2000);
}
static class Service {
void login() {
System.out.println("user:" + LoginUser.get());
}
}
}
上面的示例模擬了用戶登錄過程,使用ScopedValue.newInstance()聲明了一個ScopedValue,使用ScopedValue.where為ScopedValue設置了一個值,并使用run方法執行接下來要做的事情,以便在run()內部隨時獲取ScopedValue。在run方法中模擬了service的登錄方法,不需要傳遞參數LoginUser,直接通過LoginUser.get方法可以直接獲取當前登錄用戶的值。