柚子快報激活碼778899分享:java JDK21虛擬線程
柚子快報激活碼778899分享:java JDK21虛擬線程
目錄
虛擬線程
話題
什么是平臺線程?
什么是虛擬線程?
為什么要使用虛擬線程?
創(chuàng)建和運行虛擬線程
使用線程類和線程創(chuàng)建虛擬線程。生成器界面
使用Executor.newVirtualThreadPerTaskExecutor()方法創(chuàng)建和運行虛擬線程
調(diào)度虛擬線程和固定虛擬線程
調(diào)試虛擬線程
JDK虛擬線程的飛行記錄器事件
官方文檔的翻譯版本
官方文檔:Virtual Threads
虛擬線程
虛擬線程是輕量級線程,可以減少編寫、維護和調(diào)試高吞吐量并發(fā)應(yīng)用程序的工作量。
有關(guān)虛擬線程的背景信息,請參閱JEP444。
線程是可以調(diào)度的最小處理單元。它與其他此類單元同時運行,并且在很大程度上獨立于其他此類單元。它是java.lang.Thread的一個實例。有兩種線程,平臺線程和虛擬線程。
話題
什么是平臺線程?什么是虛擬線程?為什么要使用虛擬線程?創(chuàng)建和運行虛擬線程調(diào)度虛擬線程和固定虛擬線程調(diào)試虛擬線程虛擬線程:采用指南
什么是平臺線程?
平臺線程被實現(xiàn)為操作系統(tǒng)(OS)線程周圍的瘦包裝器。平臺線程在其底層操作系統(tǒng)線程上運行Java代碼,平臺線程在平臺線程的整個生命周期中捕獲其操作系統(tǒng)線程。因此,可用平臺線程的數(shù)量被限制為OS線程的數(shù)量。
平臺線程通常具有由操作系統(tǒng)維護的大型線程堆棧和其他資源。它們適用于運行所有類型的任務(wù),但可能是有限的資源。
什么是虛擬線程?
與平臺線程一樣,虛擬線程也是java.lang.thread的一個實例。然而,虛擬線程并沒有綁定到特定的操作系統(tǒng)線程。虛擬線程仍然在操作系統(tǒng)線程上運行代碼。但是,當(dāng)虛擬線程中運行的代碼調(diào)用阻塞I/O操作時,Java運行時會掛起虛擬線程,直到可以恢復(fù)為止。與掛起的虛擬線程相關(guān)聯(lián)的OS線程現(xiàn)在可以自由地執(zhí)行其他虛擬線程的操作。
虛擬線程的實現(xiàn)方式與虛擬內(nèi)存類似。為了模擬大量內(nèi)存,操作系統(tǒng)將大量虛擬地址空間映射到有限的RAM。同樣,為了模擬大量線程,Java運行時將大量虛擬線程映射到少量操作系統(tǒng)線程。
與平臺線程不同,虛擬線程通常有一個淺調(diào)用堆棧,只執(zhí)行一個HTTP客戶端調(diào)用或一個JDBC查詢。盡管虛擬線程支持線程本地變量和可繼承的線程本地變量,但您應(yīng)該仔細(xì)考慮使用它們,因為單個JVM可能支持?jǐn)?shù)百萬個虛擬線程。
虛擬線程適用于運行大部分時間被阻塞的任務(wù),這些任務(wù)通常等待I/O操作完成。然而,它們并不適用于長時間運行的CPU密集型操作。
為什么要使用虛擬線程?
在高吞吐量并發(fā)應(yīng)用程序中使用虛擬線程,尤其是那些由大量并發(fā)任務(wù)組成、花費大量時間等待的應(yīng)用程序。服務(wù)器應(yīng)用程序是高吞吐量應(yīng)用程序的示例,因為它們通常處理許多執(zhí)行阻塞I/O操作(如獲取資源)的客戶端請求。
虛擬線程不是更快的線程;它們運行代碼的速度并不比平臺線程快。它們的存在是為了提供規(guī)模(更高的吞吐量),而不是速度(更低的延遲)。
創(chuàng)建和運行虛擬線程
線程和線程。生成器API提供了創(chuàng)建平臺線程和虛擬線程的方法。java.util.concurrent。Executors類還定義了創(chuàng)建ExecutorService的方法,該方法為每個任務(wù)啟動一個新的虛擬線程。
話題
使用線程類和線程創(chuàng)建虛擬線程。生成器界面使用Executor.newVirtualThreadPerTaskExecutor()方法創(chuàng)建和運行虛擬線程多線程客戶端服務(wù)器示例
使用線程類和線程創(chuàng)建虛擬線程。生成器界面
調(diào)用Thread.ofVirtual()方法來創(chuàng)建Thread的實例。用于創(chuàng)建虛擬線程的生成器。
以下示例創(chuàng)建并啟動一個打印消息的虛擬線程。它調(diào)用聯(lián)接方法以等待虛擬線程終止。(這使您能夠在主線程終止之前看到打印的消息。)
Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello"));
thread.join();
生成器界面允許您創(chuàng)建具有通用線程屬性(如線程名稱)的線程。線程。建設(shè)者OfPlatform子接口創(chuàng)建平臺線程,而Thread。建設(shè)者OfVirtual創(chuàng)建虛擬線程。
以下示例使用thread創(chuàng)建一個名為MyThread的虛擬線程。生成器界面:
Thread.Builder builder = Thread.ofVirtual().name("MyThread");
Runnable task = () -> {
System.out.println("Running thread");
};
Thread t = builder.start(task);
System.out.println("Thread t name: " + t.getName());
t.join();
以下示例使用Thread創(chuàng)建并啟動兩個?Thread.Builder
Thread.Builder builder = Thread.ofVirtual().name("worker-", 0);
Runnable task = () -> {
System.out.println("Thread ID: " + Thread.currentThread().threadId());
};
// name "worker-0"
Thread t1 = builder.start(task);
t1.join();
System.out.println(t1.getName() + " terminated");
// name "worker-1"
Thread t2 = builder.start(task);
t2.join();
System.out.println(t2.getName() + " terminated");
此示例打印類似于以下內(nèi)容的輸出:
Thread ID: 21 worker-0 terminated Thread ID: 24 worker-1 terminated
使用Executor.newVirtualThreadPerTaskExecutor()方法創(chuàng)建和運行虛擬線程
執(zhí)行器允許您將線程管理和創(chuàng)建與應(yīng)用程序的其余部分分開。
以下示例使用Executors.newVirtualThreadPerTaskExecutor()方法創(chuàng)建ExecutorService。每當(dāng)調(diào)用ExecutorService.submit(Runnable)時,都會創(chuàng)建并啟動一個新的虛擬線程來運行任務(wù)。此方法返回Future的一個實例。請注意,方法Future.get()等待線程的任務(wù)完成。因此,此示例在虛擬線程的任務(wù)完成后打印一條消息。
try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
Future> future = myExecutor.submit(() -> System.out.println("Running thread"));
future.get();
System.out.println("Task completed");
// ...
多線程客戶端服務(wù)器示例
以下示例由兩個類組成。EchoServer是一個服務(wù)器程序,它偵聽端口并為每個連接啟動一個新的虛擬線程。EchoClient是一個連接到服務(wù)器并發(fā)送在命令行上輸入的消息的客戶端程序。
EchoClient創(chuàng)建一個套接字,從而連接到EchoServer。它在標(biāo)準(zhǔn)輸入流上讀取用戶的輸入,然后通過將文本寫入套接字將文本轉(zhuǎn)發(fā)到EchoServer。EchoServer通過插座將輸入回波至EchoClient。EchoClient讀取并顯示從服務(wù)器傳回的數(shù)據(jù)。EchoServer可以通過虛擬線程同時為多個客戶端提供服務(wù),每個客戶端連接一個線程
public class EchoServer {
public static void main(String[] args) throws IOException {
if (args.length != 1) {
System.err.println("Usage: java EchoServer
System.exit(1);
}
int portNumber = Integer.parseInt(args[0]);
try (
ServerSocket serverSocket =
new ServerSocket(Integer.parseInt(args[0]));
) {
while (true) {
Socket clientSocket = serverSocket.accept();
// Accept incoming connections
// Start a service thread
Thread.ofVirtual().start(() -> {
try (
PrintWriter out =
new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println(inputLine);
out.println(inputLine);
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
} catch (IOException e) {
System.out.println("Exception caught when trying to listen on port "
+ portNumber + " or listening for a connection");
System.out.println(e.getMessage());
}
}
}
public class EchoClient {
public static void main(String[] args) throws IOException {
if (args.length != 2) {
System.err.println(
"Usage: java EchoClient
System.exit(1);
}
String hostName = args[0];
int portNumber = Integer.parseInt(args[1]);
try (
Socket echoSocket = new Socket(hostName, portNumber);
PrintWriter out =
new PrintWriter(echoSocket.getOutputStream(), true);
BufferedReader in =
new BufferedReader(
new InputStreamReader(echoSocket.getInputStream()));
) {
BufferedReader stdIn =
new BufferedReader(
new InputStreamReader(System.in));
String userInput;
while ((userInput = stdIn.readLine()) != null) {
out.println(userInput);
System.out.println("echo: " + in.readLine());
if (userInput.equals("bye")) break;
}
} catch (UnknownHostException e) {
System.err.println("Don't know about host " + hostName);
System.exit(1);
} catch (IOException e) {
System.err.println("Couldn't get I/O for the connection to " +
hostName);
System.exit(1);
}
}
}
調(diào)度虛擬線程和固定虛擬線程
操作系統(tǒng)安排平臺線程何時運行。但是,Java運行時會安排虛擬線程何時運行。當(dāng)Java運行時調(diào)度虛擬線程時,它將虛擬線程分配或裝載到平臺線程上,然后操作系統(tǒng)照常調(diào)度該平臺線程。這個平臺線程被稱為載體。在運行一些代碼后,虛擬線程可以從其承載器中卸載。這種情況通常發(fā)生在虛擬線程執(zhí)行阻塞I/O操作時。虛擬線程從其載體上卸載后,載體是空閑的,這意味著Java運行時調(diào)度程序可以在其上裝載不同的虛擬線程。
當(dāng)虛擬線程固定在其承載器上時,在阻塞操作期間無法卸載它。虛擬線程在以下情況下被固定:
虛擬線程在同步塊或方法內(nèi)運行代碼虛擬線程運行本機方法或外部函數(shù)(請參閱外部函數(shù)和內(nèi)存API)
固定不會使應(yīng)用程序出錯,但可能會阻礙其可擴展性。通過修改頻繁運行的同步塊或方法,并使用java.util.concurrent.locks保護潛在的長I/O操作,嘗試避免頻繁和長期的固定。重新輸入鎖定。
調(diào)試虛擬線程
虛擬線程是靜態(tài)線程;調(diào)試器可以像平臺線程一樣逐步完成它們。JDK Flight Recorder和jcmd工具具有其他功能,可以幫助您觀察應(yīng)用程序中的虛擬線程。
話題
JDK虛擬線程的飛行記錄器事件查看jcmd線程轉(zhuǎn)儲中的虛擬線程
JDK虛擬線程的飛行記錄器事件
JDK飛行記錄器(JFR)可以發(fā)出以下與虛擬線程相關(guān)的事件:
jdk.VirtualThreadStart和jdk。VirtualThreadEnd指示虛擬線程何時開始和結(jié)束。默認(rèn)情況下,這些事件處于禁用狀態(tài)。dk.VirtualThreadPinned?表示虛擬線程被固定(其承載線程未被釋放)的時間超過閾值持續(xù)時間。此事件默認(rèn)啟用,閾值為20ms。jdk.VirtualThreadSubmitFailed表示啟動或取消標(biāo)記虛擬線程失敗,可能是由于資源問題。停放虛擬線程會釋放底層承載線程來執(zhí)行其他工作,而取消標(biāo)記虛擬線程則會調(diào)度它繼續(xù)執(zhí)行。默認(rèn)情況下會啟用此事件。
使用按請求線程樣式的阻塞I/O API編寫簡單的同步代碼
虛擬線程可以顯著提高以每請求線程方式編寫的服務(wù)器的吞吐量,而不是延遲。在這種風(fēng)格中,服務(wù)器將一個線程專門用于處理每個傳入請求的整個持續(xù)時間。它至少專用一個線程,因為在處理單個請求時,您可能希望使用更多的線程來同時執(zhí)行一些任務(wù)。
阻塞一個平臺線程是昂貴的,因為它保留了線程——一種相對稀缺的資源——而它沒有做太多有意義的工作。因為虛擬線程可能很多,所以阻塞它們是廉價的,也是受鼓勵的。因此,您應(yīng)該以直接的同步風(fēng)格編寫代碼,并使用阻塞I/O API。
例如,以下代碼以非阻塞異步風(fēng)格編寫,不會從虛擬線程中獲得太多好處。
CompletableFuture.supplyAsync(info::getUrl, pool)
.thenCompose(url -> getBodyAsync(url, HttpResponse.BodyHandlers.ofString()))
.thenApply(info::findImage)
.thenCompose(url -> getBodyAsync(url, HttpResponse.BodyHandlers.ofByteArray()))
.thenApply(info::setImageData)
.thenAccept(this::process)
.exceptionally(t -> { t.printStackTrace(); return null; });
另一方面,以下代碼以同步風(fēng)格編寫,并使用簡單的阻塞IO,將受益匪淺:
try {
String page = getBody(info.getUrl(), HttpResponse.BodyHandlers.ofString());
String imageUrl = info.findImage(page);
byte[] data = getBody(imageUrl, HttpResponse.BodyHandlers.ofByteArray());
info.setImageData(data);
process(info);
} catch (Exception ex) {
t.printStackTrace();
}
這樣的代碼也更容易在調(diào)試器中調(diào)試、在探查器中配置文件或使用線程轉(zhuǎn)儲進行觀察。要觀察虛擬線程,請使用jcmd命令創(chuàng)建一個線程轉(zhuǎn)儲:
jcmd
以這種風(fēng)格編寫的堆棧越多,虛擬線程的性能和可觀察性就越好。以其他風(fēng)格編寫的程序或框架,如果不為每個任務(wù)指定一個線程,就不應(yīng)該期望從虛擬線程中獲得顯著的好處。避免將同步、阻塞代碼與異步框架混合使用。
將每個并發(fā)任務(wù)表示為一個虛擬線程;從不池化虛擬線程
關(guān)于虛擬線程,最難內(nèi)化的是,盡管它們與平臺線程具有相同的行為,但它們不應(yīng)該表示相同的程序概念。
平臺線程是稀缺的,因此是一種寶貴的資源。需要管理寶貴的資源,而管理平臺線程的最常見方式是使用線程池。然后您需要回答的一個問題是,池中應(yīng)該有多少個線程?
但虛擬線程是豐富的,因此每個線程都不應(yīng)該代表一些共享的、池化的資源,而是一個任務(wù)。線程從托管資源變成應(yīng)用程序域?qū)ο?。我們?yīng)該有多少虛擬線程的問題變得顯而易見,就像我們應(yīng)該使用多少字符串在內(nèi)存中存儲一組用戶名的問題一樣:虛擬線程的數(shù)量總是等于應(yīng)用程序中并發(fā)任務(wù)的數(shù)量。
將n個平臺線程轉(zhuǎn)換為n個虛擬線程幾乎沒有好處;相反,需要轉(zhuǎn)換的是任務(wù)。
要將每個應(yīng)用程序任務(wù)表示為線程,請不要像下面的示例那樣使用共享線程池執(zhí)行器:
Future
Future
// ... use futures
相反,使用虛擬線程執(zhí)行器,如以下示例所示:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future
Future
// ... use futures
}
代碼仍然使用ExecutorService,但從Executors.newVirtualThreadPerTaskExecutor()返回的代碼沒有使用線程池。相反,它為每個提交的任務(wù)創(chuàng)建一個新的虛擬線程。
此外,ExecutorService本身是輕量級的,我們可以創(chuàng)建一個新的,就像處理任何簡單的對象一樣。這使我們能夠依賴于新添加的ExecutorService.close()方法和try-with-resources構(gòu)造。在try塊結(jié)束時隱式調(diào)用的close方法將自動等待提交給ExecutorService的所有任務(wù)(即ExecutorServices派生的所有虛擬線程)終止。
對于輸出場景,這是一個特別有用的模式,在輸出場景中,您希望同時執(zhí)行對不同服務(wù)的多個傳出調(diào)用,如以下示例所示:
void handle(Request request, Response response) {
var url1 = ...
var url2 = ...
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var future1 = executor.submit(() -> fetchURL(url1));
var future2 = executor.submit(() -> fetchURL(url2));
response.send(future1.get() + future2.get());
} catch (ExecutionException | InterruptedException e) {
response.fail(e);
}
}
String fetchURL(URL url) throws IOException {
try (var in = url.openStream()) {
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
}
您應(yīng)該創(chuàng)建一個新的虛擬線程,如上所示,用于即使是小的、短暫的并發(fā)任務(wù)。
為了獲得更多關(guān)于編寫輸出模式和其他具有更好可觀察性的常見并發(fā)模式的幫助,請使用結(jié)構(gòu)化并發(fā)。
根據(jù)經(jīng)驗,如果您的應(yīng)用程序從來沒有10000個或更多的虛擬線程,那么它就不太可能從虛擬線程中受益。要么它的負(fù)載太輕,需要更好的吞吐量,要么您沒有向虛擬線程表示足夠多的任務(wù)。
使用信號量限制并發(fā)
有時需要限制某個操作的并發(fā)性。例如,某些外部服務(wù)可能無法處理超過十個并發(fā)請求。由于平臺線程是一種寶貴的資源,通常在池中進行管理,線程池變得如此普遍,以至于它們被用于限制并發(fā)性,如以下示例所示:
ExecutorService es = Executors.newFixedThreadPool(10);
...
Result foo() {
try {
var fut = es.submit(() -> callLimitedService());
return f.get();
} catch (...) { ... }
}
此示例確保對有限服務(wù)最多有十個并發(fā)請求。
但限制并發(fā)只是線程池操作的副作用。池是為了共享稀缺資源而設(shè)計的,虛擬線程并不稀缺,因此永遠不應(yīng)該被池化!
在使用虛擬線程時,如果您想限制訪問某些服務(wù)的并發(fā)性,則應(yīng)該使用專門為此目的設(shè)計的構(gòu)造:Semaphore類。以下示例演示了此類:
Semaphore sem = new Semaphore(10);
...
Result foo() {
sem.acquire();
try {
return callLimitedService();
} finally {
sem.release();
}
}
碰巧調(diào)用foo的線程將被抑制,也就是說,被阻塞,這樣一次只有十個線程可以取得進展,而其他線程將不受阻礙地進行業(yè)務(wù)。
簡單地用信號量阻塞一些虛擬線程似乎與將任務(wù)提交到固定線程池有很大不同,但事實并非如此。將任務(wù)提交給線程池會使它們排隊等待稍后執(zhí)行,但信號量在內(nèi)部(或任何其他阻塞同步結(jié)構(gòu))創(chuàng)建一個被阻塞的線程隊列,該隊列反映了等待池線程執(zhí)行它們的任務(wù)隊列。由于虛擬線程是任務(wù),因此生成的結(jié)構(gòu)是等效的:
盡管您可以將平臺線程池視為處理從隊列中提取的任務(wù)的工作線程,將虛擬線程視為任務(wù)本身,直到它們可以繼續(xù),但計算機中的底層表示實際上是相同的。識別排隊任務(wù)和阻塞線程之間的等效性將有助于充分利用虛擬線程。
數(shù)據(jù)庫連接池本身就是一個信號燈。限制為十個連接的連接池將阻止第十一個線程嘗試獲取連接。不需要在連接池的頂部添加額外的信號量。
不要在線程本地變量中緩存昂貴的可重用對象
虛擬線程和平臺線程一樣支持線程本地變量。有關(guān)詳細(xì)信息,請參閱線程本地變量(thread local variables)。通常,線程局部變量用于將一些特定于上下文的信息與當(dāng)前運行的代碼相關(guān)聯(lián),例如當(dāng)前事務(wù)和用戶ID。這種線程局部變量的使用對于虛擬線程來說是完全合理的。但是,請考慮使用更安全、更高效的作用域值。有關(guān)詳細(xì)信息,請參閱作用域值。
線程局部變量的另一種用法與虛擬線程根本不同:緩存可重用對象。這些對象的創(chuàng)建成本通常很高(并且會消耗大量內(nèi)存),是可變的,并且不是線程安全的。它們被緩存在線程局部變量中,以減少它們被實例化的次數(shù)和內(nèi)存中的實例數(shù)量,但它們會被線程上不同時間運行的多個任務(wù)重用。
例如,SimpleDateFormat的實例的創(chuàng)建成本很高,而且不是線程安全的。出現(xiàn)的一種模式是將這樣的實例緩存在ThreadLocal中,如以下示例所示:
static final ThreadLocal
ThreadLocal.withInitial(SimpleDateFormat::new);
void foo() {
...
cachedFormatter.get().format(...);
...
}
只有當(dāng)線程(以及緩存在線程本地中的昂貴對象)被多個任務(wù)共享和重用時,這種緩存才有幫助,就像平臺線程被池化時一樣。許多任務(wù)在線程池中運行時可能會調(diào)用foo,但由于線程池只包含幾個線程,因此對象只會實例化幾次——每個池線程一次——緩存并重用。
然而,虛擬線程從不被池化,也從不被不相關(guān)的任務(wù)重用。因為每個任務(wù)都有自己的虛擬線程,所以來自不同任務(wù)的每次對foo的調(diào)用都會觸發(fā)新SimpleDateFormat的實例化。此外,由于可能有大量虛擬線程同時運行,因此昂貴的對象可能會消耗相當(dāng)多的內(nèi)存。這些結(jié)果與線程本地緩存所要實現(xiàn)的結(jié)果正好相反。
沒有單一的通用替代方案可供選擇,但在SimpleDateFormat的情況下,您應(yīng)該將其替換為DateTimeFormatter。DateTimeFormatter是不可變的,因此所有線程都可以共享一個實例:
static final DateTimeFormatter formatter = DateTimeFormatter….;
void foo() {
...
formatter.format(...);
...
}
請注意,使用線程局部變量緩存共享的昂貴對象有時是由異步框架在幕后完成的,因為異步框架隱含地假設(shè)它們由極少數(shù)池線程使用。這就是為什么混合虛擬線程和異步框架不是一個好主意的原因之一:對方法的調(diào)用可能會導(dǎo)致在線程本地變量中實例化旨在緩存和共享的代價高昂的對象。
避免長時間和頻繁的固定
當(dāng)前虛擬線程實現(xiàn)的一個限制是,在同步塊或方法內(nèi)部執(zhí)行阻塞操作會導(dǎo)致JDK的虛擬線程調(diào)度程序阻塞寶貴的操作系統(tǒng)線程,而如果阻塞操作在同步塊和方法之外執(zhí)行,則不會阻塞。我們稱這種情況為“釘扎”。如果阻塞操作是長期且頻繁的,則固定可能會對服務(wù)器的吞吐量產(chǎn)生不利影響。保護短暫的操作,如內(nèi)存中的操作,或具有同步塊或方法的不頻繁操作,應(yīng)該不會產(chǎn)生不利影響。
為了檢測可能有害的釘扎實例,(JDK飛行記錄器(JFR)會發(fā)出JDK。VirtualThreadPined線程當(dāng)鎖定了阻塞操作時;默認(rèn)情況下,當(dāng)操作耗時超過20ms時,會啟用此事件。
或者,可以使用系統(tǒng)屬性jdk.tracePinnedThreads在線程被固定時阻塞時發(fā)出堆棧跟蹤。使用選項Djdk.tracePinnedThreads=full運行時,當(dāng)線程在固定時發(fā)生阻塞時,將打印完整的堆棧跟蹤,突出顯示本地幀和持有監(jiān)視器的幀。使用選項Djdk.tracePinnedThreads=short運行會將輸出僅限于有問題的幀。
如果這些機制檢測到固定既長時間又頻繁的位置,請在這些特定位置使用synchronized with ReentrantLock來替換(同樣,在保護短時間或不頻繁操作的位置,無需替換synchronized)。以下是同步化塊的長期頻繁使用示例。
synchronized(lockObj) {
frequentIO();
}
lock.lock();
try {
frequentIO();
} finally {
lock.unlock();
}
柚子快報激活碼778899分享:java JDK21虛擬線程
文章鏈接
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點和立場。
轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。