Hello folks,我是 Luga,今天我們來聊一下 JAVA 生態的核心技術—— Java Virtual Threads,即 “Java 虛擬線程” 。
虛擬線程是 Java 中的一個重要創新,在 Project Loom 項目中開發的。自從 Java 19 開始作為預覽功能引入,到 Java 21 以后成為正式版本(JEP 444),虛擬線程已經成為 JDK 的一部分。
一、為什么是 Java Virtual Threads ?
眾所周知,JVM 是一個多線程環境,通過 java.lang.Thread 類型為我們提供了對操作系統線程的抽象。在 Project Loom 之前,JVM 中的每個線程都只是對操作系統線程的一種簡單封裝,我們可以稱之為“平臺線程”。
然而,所謂的“平臺線程”,在某些特定的業務場景中,往往存在一些問題,從多個角度來看,它們都是昂貴的。首先,創建平臺線程的成本很高。每當創建一個平臺線程時,操作系統必須在堆棧中分配大量內存(以兆字節計)來存儲線程的上下文、原生調用堆棧和 Java 調用堆棧。由于堆棧大小是固定的,這就導致了高昂的內存開銷。此外,每當調度器對線程進行搶占式調度時,也需要移動大量的內存。
因此,我們可以想象,這在空間和時間上都是非常昂貴的操作。實際上,由于堆棧框架的巨大尺寸限制,我們對可創建的線程數量也存在限制。在 Java 中,我們很容易遇到 OutOfMemoryError,只需不斷實例化新的平臺線程,直到操作系統的內存耗盡為止。
private static void stackOverFlowErrorExample() {
for (int i = 0; i < 100_000; i++) {
new Thread(() -> {
try {
Thread.sleep(Duration.ofSeconds(1L));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
}
由于平臺線程的創建成本較高,每個線程需要分配一定數量的堆棧內存,因此在某些情況下,如果我們不斷實例化新的平臺線程,直到操作系統的內存耗盡,就有可能迅速觸發 OutOfMemoryError。
然而,這個過程的確切時間取決于多個因素,包括可用的內存大小、操作系統的線程限制以及 JVM 的配置。如果可用的內存較小,同時 JVM 的堆大小也較小,那么在不斷實例化新的平臺線程時,很可能會很快達到內存的極限,導致 OutOfMemoryError 的發生。
[0.949s][warning][os,thread] FAIled to start thread "Unknown thread" - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 4k, detached.
[0.949s][warning][os,thread] Failed to start the native thread for java.lang.Thread "Thread-4073"
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
上述示例展示了我們如何基于當前的受到限制的環境中進行并發編程。
然而,Java 自從問世以來一直致力于成為一種簡單易用的編程語言。在并發編程領域,我們應該像編寫順序代碼一樣編寫程序。事實上,在 Java 中,為每個并發任務創建一個新線程是編寫并發程序更簡單的方法之一。這種模型被稱為"每個線程一個任務"。
接下來,我們來看一下虛擬線程內部架構,具體如下所示:
使用這種方法,每個線程可以使用自己的局部變量來存儲信息,從而大大減少了共享可變狀態的需求。線程之間共享狀態是并發編程中眾所周知的"棘手部分"。然而,通過每個線程一個任務的模型,我們可以輕松地避免復雜的線程同步和共享狀態的問題。
然而,正如之前提到的,使用這種方法也存在著限制,即我們能夠創建的線程數量有限。由于平臺線程的創建成本較高,每個線程都需要分配一定數量的堆棧內存,這限制了我們可以創建的線程數量。如果我們不加限制地創建大量線程,就有可能導致內存耗盡和性能下降。
需要注意的是,隨著 Project Loom 的引入,虛擬線程的輕量級特性將顯著改善線程創建成本和內存開銷。這將使我們能夠更輕松地創建大規模的并發任務,而不會受到線程數量限制的困擾。
二、那么,如何創建 Virtual Threads ?
正如我們之前所提到的,虛擬線程是一種新型的線程,旨在解決平臺線程的資源限制問題。它們是 java.lang.Thread 類型的替代實現,將堆幀(Heap Frame)存儲在堆內存中,而不是堆棧中。
由于虛擬線程的堆棧存儲在堆中,因此它們的初始內存占用非常小,通常只有幾百字節,而不是兆字節。此外,堆棧塊的大小可以動態調整。這意味著我們不需要為每個可能的用例分配數百兆字節的內存。
通常而言,創建一個新的虛擬線程非常簡單。我們可以使用 java.lang.Thread類 型上的新工廠方法 ofVirtual 來實現。讓我們首先定義一個實用函數,用于創建具有給定名稱的虛擬線程的示例代碼:
import java.lang.Thread;
public class VirtualThreadExample {
public static void main(String[] args) {
Thread virtualThread = Thread.ofVirtual("VirtualThreadExample", VirtualThreadExample::runTask);
virtualThread.start();
}
public static void runTask() {
// 在虛擬線程中執行的任務代碼
System.out.println("Running task in virtual thread");
}
}
在上面的示例中,我們使用 Thread.ofVirtual 方法創建了一個名為 "VirtualThreadExample" 的虛擬線程,并指定了要在其中執行的任務代碼。然后,我們調用 start 方法啟動虛擬線程。
通過使用虛擬線程,我們可以更加靈活地管理線程的內存消耗,并提高并發程序的性能和可伸縮性。虛擬線程是 Project Loom 的關鍵特性之一,將極大地改善 Java 中的并發編程體驗。
三、Virtual Threads 到底有哪些方面優勢?
作為 Project Loom 提出的一種新的線程模型,即虛擬線程。虛擬線程是一種輕量級的線程,其堆棧存儲在堆內存中,而不是在操作系統線程的堆棧中。這種設計使得虛擬線程的創建和銷毀成本較低,并且可以創建大量的線程,而不會受到操作系統和硬件資源的限制。
虛擬線程的引入將改變 Java 中的并發編程方式。它們可以通過更高效地利用系統資源來提高并發性能,并且可以簡化并發編程的復雜性。虛擬線程可以使用更少的內存,并且可以根據需求動態調整堆棧的大小,以提高資源利用率。
具體可參考如下所示:
1.減少應用程序內存消耗
與傳統的由平臺線程都映射到操作系統線程的生命周期相對比,虛擬線程通過較小的初始內存占用、動態調整堆棧大小、共享堆棧和更高效的內存管理等方式,減少了應用程序的內存消耗。這使得可以創建更多的線程,提高并發性能,并且更有效地利用系統資源。
2.提高應用程序吞吐量
在大多數架構中,應用程序可以處理的請求數量與應用程序服務器線程池中可用的線程數量成正比。因為每個客戶請求都由單個唯一的線程處理。因此,如果可用的線程數量較少,則只能同時處理少量請求。這將降低應用程序的吞吐量。另一方面,如果應用程序服務器線程池配置了Java虛擬線程,它可以創建明顯更高的線程數量(數百萬),這將最終提高應用程序的吞吐量。
此外,在某些應用程序中,應用程序服務器線程池中的可用線程在其他計算資源(如CPU、內存、網絡、存儲)飽和之前首先飽和。對于這樣的虛擬線程來說,這將是一個較大的增強。
3.減少無法創建新的本機線程的 “OutOfMemoryError” 異常
在 JVM 上運行的應用程序容易出現“java.lang.OutOfMemoryError:無法創建新的本機線程”。這種類型的內存錯誤通常發生在如下兩種情況下:
- 當應用程序創建的線程超過服務器(或容器)的 RAM 容量時
- 當應用程序創建的線程超過操作系統允許的限制時(注:在操作系統中,有一個內核限制,該限制規定了單個進程可以創建的線程數量)。
通常而言,Java 虛擬線程在減少內存消耗方面具有顯著優勢。相比傳統的平臺線程,Java 虛擬線程通常更輕量級,它們占用的內存較少。這使得使用虛擬線程比使用平臺線程更難達到 RAM 容量的飽和。
傳統的平臺線程需要分配操作系統線程,并且每個線程都有一定的內存開銷。而虛擬線程在不做實際工作時,并不需要分配操作系統線程,因此虛擬線程應用程序超過操作系統線程限制的可能性要遠遠高于傳統的平臺線程。
虛擬線程的輕量級特性和更高的靈活性使得可以創建更多的線程,而不會受到操作系統和硬件資源的限制。這進一步增加了虛擬線程應用程序處理大規模并發的能力,提高了系統的可伸縮性。
4.提高應用程序可用性
在我們主流的系統架構中,應用程序通常需要與多個后端系統進行通信,如 API、數據庫和第三方框架等。然而,當其中一個后端系統出現中斷或響應緩慢時,傳統的應用程序服務器線程會被阻塞,等待后端系統的響應。隨著更多請求進入應用程序,越來越多的線程會被阻塞。在這種情況下,應用程序服務器線程池中的線程數量是有限的。如果所有線程都被阻塞等待后端系統的響應,那么就沒有可用線程來處理新的請求,從而導致整個應用程序不可用。
然而,通過將應用程序服務器線程池配置為使用 Java 虛擬線程,可以解決上述問題并提高應用程序的可用性。使用虛擬線程,我們甚至可以輕松創建數百萬個線程,而不會出現重大問題。當虛擬線程被阻塞等待后端系統的響應時,它會像任何其他應用程序對象一樣,以非常輕量級的方式存儲在 Java 堆區域中。因此,應用程序服務器線程池可以繼續創建虛擬線程,而不會耗盡線程池中的線程資源,直到后端系統恢復。
這種優化策略為應用程序帶來了巨大的潛力,提高了應用程序的可用性。即使在后端系統出現問題時,應用程序仍然能夠繼續創建和處理請求,而不會因為線程資源的耗盡而導致不可用狀態。這種靈活性和彈性使得應用程序能夠更好地應對高負載和故障情況,保持穩定的運行狀態。
Java 虛擬線程提供了現代應用程序所需的強大且高效的并發模型。它簡化了并發編程,并帶來更好的資源利用率,因此有可能徹底改變開發人員在 Java 中處理并發代碼的方式。
隨著 Java 技術不斷發展和創新,了解最新的功能如虛擬線程對于那些希望保持領先地位并充分利用 Java 生態系統潛力的開發人員來說至關重要。
虛擬線程提供了一種輕量級的線程模型,通過協作調度和高效的內存管理,大大減少了線程創建和管理的開銷。這使得開發人員能夠更容易地編寫高性能、高并發的應用程序,而無需擔心傳統線程模型的限制和開銷。
通過使用虛擬線程,開發人員可以更好地利用系統資源,提高應用程序的并發性能。虛擬線程的出現為 Java 生態系統帶來了更多的潛力和機會,使得開發人員能夠更好地應對現代應用程序中的并發需求。
因此,對于那些希望保持領先并充分利用 Java 生態系統的開發人員來說,了解虛擬線程等先進功能是至關重要的。這將使他們能夠更好地應對并發編程挑戰,并構建出高性能、可擴展的應用程序,從而在競爭激烈的軟件開發市場中脫穎而出。