您好,登錄后才能下訂單哦!
這篇“Java之JDK19虛擬線程的知識點有哪些”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“Java之JDK19虛擬線程的知識點有哪些”文章吧。
介紹
虛擬線程具有和 Go 語言的 goroutines 和 Erlang 語言的進程類似的實現方式,它們是用戶模式(user-mode)線程的一種形式。
在過去 Java 中常常使用線程池來進行平臺線程的共享以提高對計算機硬件的使用率,但在這種異步風格中,請求的每個階段可能在不同的線程上執行,每個線程以交錯的方式運行屬于不同請求的階段,與 Java 平臺的設計不協調從而導致:
堆棧跟蹤不提供可用的上下文
調試器不能單步執行請求處理邏輯
分析器不能將操作的成本與其調用方關聯。
而虛擬線程既保持與平臺的設計兼容,同時又能最佳地利用硬件從而不影響可伸縮性。虛擬線程是由 JDK 而非操作系統提供的線程的輕量級實現:
虛擬線程是沒有綁定到特定操作系統線程的線程。
平臺線程是以傳統方式實現的線程,作為圍繞操作系統線程的簡單包裝。
摘要
向 Java 平臺引入虛擬線程。虛擬線程是輕量級線程,它可以大大減少編寫、維護和觀察高吞吐量并發應用程序的工作量。
目標
允許以簡單的每個請求一個線程的方式編寫的服務器應用程序以接近最佳的硬件利用率進行擴展。
允許使用 java.lang.ThreadAPI 的現有代碼采用虛擬線程,并且只做最小的更改。
使用現有的 JDK 工具可以方便地對虛擬線程進行故障排除、調試和分析。
非目標
移除線程的傳統實現或遷移現有應用程序以使用虛擬線程并不是目標。
改變 Java 的基本并發模型。
我們的目標不是在 Java 語言或 Java 庫中提供新的資料平行結構。StreamAPI 仍然是并行處理大型數據集的首選方法。
動機
近30年來,Java 開發人員一直依賴線程作為并發服務器應用程序的構件。每個方法中的每個語句都在一個線程中執行,而且由于 Java 是多線程的,因此執行的多個線程同時發生。
線程是 Java 的并發單元: 一段順序代碼,與其他這樣的單元并發運行,并且在很大程度上獨立于這些單元。
每個線程都提供一個堆棧來存儲本地變量和協調方法調用,以及出錯時的上下文: 異常被同一個線程中的方法拋出和捕獲,因此開發人員可以使用線程的堆棧跟蹤來查找發生了什么。
線程也是工具的一個核心概念: 調試器遍歷線程方法中的語句,分析器可視化多個線程的行為,以幫助理解它們的性能。
thread-per-request style
服務器應用程序通常處理彼此獨立的并發用戶請求,因此應用程序通過在整個請求持續期間為該請求分配一個線程來處理請求是有意義的。這種按請求執行線程的 style 易于理解、易于編程、易于調試和配置,因為它使用平臺的并發單元來表示應用程序的并發單元。
服務器應用程序的可伸縮性受到利特爾定律(Little's Law)的支配,該定律關系到延遲、并發性和吞吐量: 對于給定的請求處理持續時間(延遲) ,應用程序同時處理的請求數(并發性) 必須與到達速率(吞吐量) 成正比增長。
例如,假設一個平均延遲為 50ms 的應用程序通過并發處理 10 個請求實現每秒 200 個請求的吞吐量。為了使該應用程序的吞吐量達到每秒 2000 個請求,它將需要同時處理 100 個請求。如果在請求持續期間每個請求都在一個線程中處理,那么為了讓應用程序跟上,線程的數量必須隨著吞吐量的增長而增長。
不幸的是,可用線程的數量是有限的,因為 JDK 將線程實現為操作系統(OS)線程的包裝器。操作系統線程代價高昂,因此我們不能擁有太多線程,這使得實現不適合每個請求一個線程的 style 。
如果每個請求在其持續時間內消耗一個線程,從而消耗一個 OS 線程,那么線程的數量通常會在其他資源(如 CPU 或網絡連接)耗盡之前很久成為限制因素。JDK 當前的線程實現將應用程序的吞吐量限制在遠低于硬件所能支持的水平。即使在線程池中也會發生這種情況,因為池有助于避免啟動新線程的高成本,但不會增加線程的總數。
asynchronous style
一些希望充分利用硬件的開發人員已經放棄了每個請求一個線程(thread-per-request) 的 style ,轉而采用線程共享(thread-sharing ) 的 style 。
請求處理代碼不是從頭到尾處理一個線程上的請求,而是在等待 I/O 操作完成時將其線程返回到一個池中,以便該線程能夠處理其他請求。這種細粒度的線程共享(其中代碼只在執行計算時保留一個線程,而不是在等待 I/O 時保留該線程)允許大量并發操作,而不需要消耗大量線程。
雖然它消除了操作系統線程的稀缺性對吞吐量的限制,但代價很高: 它需要一種所謂的異步編程 style ,采用一組獨立的 I/O 方法,這些方法不等待 I/O 操作完成,而是在以后將其完成信號發送給回調。如果沒有專門的線程,開發人員必須將請求處理邏輯分解成小的階段,通常以 lambda 表達式的形式編寫,然后將它們組合成帶有 API 的順序管道(例如,參見 CompletableFuture,或者所謂的“反應性”框架)。因此,它們放棄了語言的基本順序組合運算符,如循環和 try/catch 塊。
在異步樣式中,請求的每個階段可能在不同的線程上執行,每個線程以交錯的方式運行屬于不同請求的階段。這對于理解程序行為有著深刻的含義:
堆棧跟蹤不提供可用的上下文
調試器不能單步執行請求處理邏輯
分析器不能將操作的成本與其調用方關聯。
當使用 Java 的流 API 在短管道中處理數據時,組合 lambda 表達式是可管理的,但是當應用程序中的所有請求處理代碼都必須以這種方式編寫時,就有問題了。這種編程 style 與 Java 平臺不一致,因為應用程序的并發單元(異步管道)不再是平臺的并發單元。
對比
使用虛擬線程保留thread-per-request style
為了使應用程序能夠在與平臺保持和諧的同時進行擴展,我們應該通過更有效地實現線程來努力保持每個請求一個線程的 style ,以便它們能夠更加豐富。
操作系統無法更有效地實現 OS 線程,因為不同的語言和運行時以不同的方式使用線程堆棧。然而,Java 運行時實現 Java 線程的方式可以切斷它們與操作系統線程之間的一一對應關系。正如操作系統通過將大量虛擬地址空間映射到有限數量的物理 RAM 而給人一種內存充足的錯覺一樣,Java 運行時也可以通過將大量虛擬線程映射到少量操作系統線程而給人一種線程充足的錯覺。
虛擬線程是沒有綁定到特定操作系統線程的線程。
平臺線程是以傳統方式實現的線程,作為圍繞操作系統線程的簡單包裝。
thread-per-request 樣式的應用程序代碼可以在整個請求期間在虛擬線程中運行,但是虛擬線程只在 CPU 上執行計算時使用操作系統線程。其結果是與異步樣式相同的可伸縮性,除了它是透明實現的:
當在虛擬線程中運行的代碼調用 Java.* API 中的阻塞 I/O 操作時,運行時執行一個非阻塞操作系統調用,并自動掛起虛擬線程,直到稍后可以恢復。
對于 Java 開發人員來說,虛擬線程是創建成本低廉、數量幾乎無限多的線程。硬件利用率接近最佳,允許高水平的并發性,從而提高吞吐量,而應用程序仍然與 Java 平臺及其工具的多線程設計保持協調。
虛擬線程的意義
虛擬線程是廉價和豐富的,因此永遠不應該被共享(即使用線程池) : 應該為每個應用程序任務創建一個新的虛擬線程。
因此,大多數虛擬線程的壽命都很短,并且具有淺層調用堆棧,執行的操作只有單個 HTTP 客戶機調用或單個 JDBC 查詢那么少。相比之下,平臺線程是重量級和昂貴的,因此經常必須共享。它們往往是長期存在的,具有深度調用堆棧,并且在許多任務之間共享。
總之,虛擬線程保留了可靠的 thread-per-request style ,這種 style 與 Java 平臺的設計相協調,同時又能最佳地利用硬件。使用虛擬線程并不需要學習新的概念,盡管它可能需要為應對當今線程的高成本而養成的忘卻習慣。虛擬線程不僅可以幫助應用程序開發人員ーー它們還可以幫助框架設計人員提供易于使用的 API,這些 API 與平臺的設計兼容,同時又不影響可伸縮性。
說明
如今,java.lang 的每一個實例。JDK 中的線程是一個平臺線程。平臺線程在底層操作系統線程上運行 Java 代碼,并在代碼的整個生命周期中捕獲操作系統線程。平臺線程的數量僅限于操作系統線程的數量。
虛擬線程是 java.lang 的一個實例。在基礎操作系統線程上運行 Java 代碼,但在代碼的整個生命周期中不捕獲該操作系統線程的線程。這意味著許多虛擬線程可以在同一個 OS 線程上運行它們的 Java 代碼,從而有效地共享它們。平臺線程壟斷了一個珍貴的操作系統線程,而虛擬線程卻沒有。虛擬線程的數量可能比操作系統線程的數量大得多。
虛擬線程是由 JDK 而非操作系統提供的線程的輕量級實現。它們是用戶模式(user-mode)線程的一種形式,已經在其他多線程語言中取得了成功(例如,Go 中的 goroutines 和 Erlang 的進程)。在 Java 的早期版本中,用戶模式線程甚至以所謂的“綠線程”為特色,當時 OS 線程還不成熟和普及。然而,Java 的綠色線程都共享一個 OS 線程(M: 1調度) ,并最終被平臺線程超越,實現為 OS 線程的包裝器(1:1調度)。虛擬線程采用 M: N 調度,其中大量(M)虛擬線程被調度在較少(N)操作系統線程上運行。
虛擬線程 VS 平臺線程
簡單示例
開發人員可以選擇使用虛擬線程還是平臺線程。下面是一個創建大量虛擬線程的示例程序。該程序首先獲得一個 ExecutorService,它將為每個提交的任務創建一個新的虛擬線程。然后,它提交10000項任務,等待所有任務完成:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 10000).forEach(i -> { executor.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); return i; }); }); } // executor.close() is called implicitly, and waits
本例中的任務是簡單的代碼(休眠一秒鐘) ,現代硬件可以輕松支持10,000個虛擬線程并發運行這些代碼。在幕后,JDK 在少數操作系統線程上運行代碼,可能只有一個線程。
如果這個程序使用 ExecutorService 為每個任務創建一個新的平臺線程,比如 Executors.newCachedThreadPool () ,那么情況就會大不相同。ExecutorService 將嘗試創建10,000個平臺線程,從而創建10,000個 OS 線程,程序可能會崩潰,這取決于計算機和操作系統。
相反,如果程序使用從池中獲取平臺線程的 ExecutorService (例如 Executors.newFixedThreadPool (200)) ,情況也不會好到哪里去。ExecutorService 將創建200個平臺線程,由所有10,000個任務共享,因此許多任務將按順序運行,而不是并發運行,而且程序將需要很長時間才能完成。對于這個程序,一個有200個平臺線程的池只能達到每秒200個任務的吞吐量,而虛擬線程達到每秒10,000個任務的吞吐量(在充分預熱之后)。此外,如果示例程序中的10000被更改為1000000,那么該程序將提交1,000,000個任務,創建1,000,000個并發運行的虛擬線程,并且(在足夠的預熱之后)實現大約1,000,000任務/秒的吞吐量。
如果這個程序中的任務執行一秒鐘的計算(例如,對一個巨大的數組進行排序)而不僅僅是休眠,那么增加超出處理器核心數量的線程數量將無濟于事,無論它們是虛擬線程還是平臺線程。
虛擬線程并不是更快的線程ーー它們運行代碼的速度并不比平臺線程快。它們的存在是為了提供規模(更高的吞吐量) ,而不是速度(更低的延遲) 。它們的數量可能比平臺線程多得多,因此根據 Little’s Law,它們能夠實現更高吞吐量所需的更高并發性。
換句話說,虛擬線程可以顯著提高應用程序的吞吐量,在如下情況時:
并發任務的數量很多(超過幾千個)
工作負載不受 CPU 限制,因為在這種情況下,比處理器核心擁有更多的線程并不能提高吞吐量
虛擬線程有助于提高典型服務器應用程序的吞吐量,因為這類應用程序由大量并發任務組成,這些任務花費了大量時間等待。
虛擬線程可以運行平臺線程可以運行的任何代碼。特別是,虛擬線程支持線程本地變量和線程中斷,就像平臺線程一樣。這意味著處理請求的現有 Java 代碼很容易在虛擬線程中運行。許多服務器框架將選擇自動執行此操作,為每個傳入請求啟動一個新的虛擬線程,并在其中運行應用程序的業務邏輯。
下面是一個服務器應用程序示例,它聚合了另外兩個服務的結果。假設的服務器框架(未顯示)為每個請求創建一個新的虛擬線程,并在該虛擬線程中運行應用程序的句柄代碼。然后,應用程序代碼創建兩個新的虛擬線程,通過與第一個示例相同的 ExecutorService 并發地獲取資源:
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); } }
這樣的服務器應用程序使用簡單的阻塞代碼,可以很好地擴展,因為它可以使用大量虛擬線程。
NewVirtualThreadPerTaskExector ()并不是創建虛擬線程的唯一方法。新的 java.lang.Thread.Builder。可以創建和啟動虛擬線程。此外,結構化并發提供了一個更強大的 API 來創建和管理虛擬線程,特別是在類似于這個服務器示例的代碼中,通過這個 API,平臺及其工具可以了解線程之間的關系。
虛擬線程是一個預覽 API,默認情況下是禁用的
上面的程序使用 Executors.newVirtualThreadPerTaskExector ()方法,因此要在 JDK 19上運行它們,必須啟用以下預覽 API:
使用javac --release 19 --enable-preview Main.java編譯該程序,并使用 java --enable-preview Main 運行該程序;或者:
在使用源代碼啟動程序時,使用 java --source 19 --enable-preview Main.java 運行程序; 或者:
在使用 jshell 時,使用 jshell --enable-preview 啟動它。
不要共享(pool)虛擬線程
開發人員通常會將應用程序代碼從傳統的基于線程池的 ExecutorService 遷移到每個任務一個虛擬線程的 ExecutorService。與所有資源池一樣,線程池旨在共享昂貴的資源,但虛擬線程并不昂貴,而且從不需要共享它們。
開發人員有時使用線程池來限制對有限資源的并發訪問。例如,如果一個服務不能處理超過20個并發請求,那么通過提交給大小為 20 的池的任務將確保執行對該服務的所有訪問。因為平臺線程的高成本使得線程池無處不在,所以這個習慣用法也變得無處不在,但是開發人員不應該為了限制并發性而將虛擬線程集中起來。應該使用專門為此目的設計的構造(如信號量semaphores)來保護對有限資源的訪問。這比線程池更有效、更方便,也更安全,因為不存在線程本地數據從一個任務意外泄漏到另一個任務的風險。
觀測
編寫清晰的代碼并不是故事的全部。對于故障排除、維護和優化來說,清晰地表示正在運行的程序的狀態也是必不可少的,JDK 長期以來一直提供調試、概要分析和監視線程的機制。這樣的工具對虛擬線程也應該這樣做ーー也許要適應它們的大量數據ーー因為它們畢竟是 java.lang.Thread 的實例。
Java 調試器可以單步執行虛擬線程、顯示調用堆棧和檢查堆棧幀中的變量。JDK Flight Recorder (JFR) 是 JDK 的低開銷分析和監視機制,可以將來自應用程序代碼的事件(比如對象分配和 I/O 操作)與正確的虛擬線程關聯起來。
這些工具不能為以異步樣式編寫的應用程序做這些事情。在這種風格中,任務與線程無關,因此調試器不能顯示或操作任務的狀態,分析器也不能告訴任務等待 I/O 所花費的時間。
線程轉儲( thread dump) 是另一種流行的工具,用于以每個請求一個線程的樣式編寫的應用程序的故障排除。遺憾的是,通過 jstack 或 jcmd 獲得的 JDK 傳統線程轉儲提供了一個扁平的線程列表。這適用于數十或數百個平臺線程,但不適用于數千或數百萬個虛擬線程。因此,我們將不會擴展傳統的線程轉儲以包含虛擬線程,而是在 jcmd 中引入一種新的線程轉儲,以顯示平臺線程旁邊的虛擬線程,所有這些線程都以一種有意義的方式進行分組。當程序使用結構化并發時,可以顯示線程之間更豐富的關系。
因為可視化和分析大量的線程可以從工具中受益,所以 jcmd 除了純文本之外,還可以發布 JSON 格式的新線程轉儲:
$ jcmdThread.dump_to_file -format=json
新的線程轉儲格式列出了在網絡 I/O 操作中被阻塞的虛擬線程,以及由上面所示的 new-thread-per-task ExecutorService 創建的虛擬線程。它不包括對象地址、鎖、 JNI 統計信息、堆統計信息以及傳統線程轉儲中出現的其他信息。此外,由于可能需要列出大量線程,因此生成新的線程轉儲并不會暫停應用程序。
下面是這樣一個線程轉儲的示例,它取自類似于上面第二個示例的應用程序,在 JSON 查看器中呈現 :
由于虛擬線程是在 JDK 中實現的,并且不綁定到任何特定的操作系統線程,因此它們對操作系統是不可見的,操作系統不知道它們的存在。操作系統級別的監視將觀察到,JDK 進程使用的操作系統線程比虛擬線程少。
調度
為了完成有用的工作,需要調度一個線程,也就是分配給處理器核心執行。對于作為 OS 線程實現的平臺線程,JDK 依賴于 OS 中的調度程序。相比之下,對于虛擬線程,JDK 有自己的調度程序。JDK 的調度程序不直接將虛擬線程分配給處理器,而是將虛擬線程分配給平臺線程(這是前面提到的虛擬線程的 M: N 調度)。然后,操作系統像往常一樣調度平臺線程。
JDK 的虛擬線程調度程序是一個在 FIFO 模式下運行的工作竊取(work-stealing) 的 ForkJoinPool。調度程序的并行性是可用于調度虛擬線程的平臺線程的數量。默認情況下,它等于可用處理器的數量,但是可以使用系統屬性 jdk.viralThreadScheduler.allelism 對其進行調優。注意,這個 ForkJoinPool 不同于公共池,例如,公共池用于并行流的實現,公共池以 LIFO 模式運行。
虛擬線程無法獲得載體(即負責調度虛擬線程的平臺線程)的標識。由 Thread.currentThread ()返回的值始終是虛擬線程本身。
載體和虛擬線程的堆棧跟蹤是分離的。在虛擬線程中拋出的異常將不包括載體的堆棧幀。線程轉儲不會顯示虛擬線程堆棧中其載體的堆棧幀,反之亦然。
虛擬線程不能使用載體的線程本地變量,反之亦然。
此外,從 Java 代碼的角度來看,虛擬線程及其載體平臺線程臨時共享操作系統線程的事實是不存在的。相比之下,從本機代碼的角度來看,虛擬線程及其載體都在同一個本機線程上運行。因此,在同一虛擬線程上多次調用的本機代碼可能會在每次調用時觀察到不同的 OS 線程標識符。
調度程序當前沒有實現虛擬線程的時間共享。分時是對消耗了分配的 CPU 時間的線程的強制搶占。雖然在平臺線程數量相對較少且 CPU 利用率為100% 的情況下,分時可以有效地減少某些任務的延遲,但是對于一百萬個虛擬線程來說,分時是否有效尚不清楚。
執行
要利用虛擬線程,不必重寫程序。虛擬線程不需要或期望應用程序代碼顯式地將控制權交還給調度程序; 換句話說,虛擬線程不是可協作的。用戶代碼不能假設如何或何時將虛擬線程分配給平臺線程,就像它不能假設如何或何時將平臺線程分配給處理器核心一樣。
為了在虛擬線程中運行代碼,JDK 的虛擬線程調度程序通過將虛擬線程掛載到平臺線程上來分配要在平臺線程上執行的虛擬線程。這使得平臺線程成為虛擬線程的載體。稍后,在運行一些代碼之后,虛擬線程可以從其載體卸載。此時平臺線程是空閑的,因此調度程序可以在其上掛載不同的虛擬線程,從而使其再次成為載體。
通常,當虛擬線程阻塞 I/O 或 JDK 中的其他阻塞操作(如 BlockingQueue.take ())時,它將卸載。當阻塞操作準備完成時(例如,在套接字上已經接收到字節) ,它將虛擬線程提交回調度程序,調度程序將在運營商上掛載虛擬線程以恢復執行。
虛擬線程的掛載和卸載頻繁且透明,并且不會阻塞任何 OS 線程。例如,前面顯示的服務器應用程序包含以下代碼行,其中包含對阻塞操作的調用:
response.send(future1.get() + future2.get());
這些操作將導致虛擬線程多次掛載和卸載,通常每個 get ()調用一次,在 send (...)中執行 I/O 過程中可能多次掛載和卸載。
JDK 中的絕大多數阻塞操作將卸載虛擬線程,從而釋放其載體和底層操作系統線程,使其承擔新的工作。但是,JDK 中的一些阻塞操作不會卸載虛擬線程,因此阻塞了其載體和底層 OS 線程。這是由于操作系統級別(例如,許多文件系統操作)或 JDK 級別(例如,Object.wait ())的限制造成的。這些阻塞操作的實現將通過暫時擴展調度程序的并行性來補償對 OS 線程的捕獲。因此,調度程序的 ForkJoinPool 中的平臺線程的數量可能會暫時超過可用處理器的數量。可以使用系統屬性 jdk.viralThreadScheduler.maxPoolSize 調優調度程序可用的最大平臺線程數。
有兩種情況下,在阻塞操作期間無法卸載虛擬線程,因為它被固定在其載體上:
當它在同步塊或方法內執行代碼時,或
當它執行本機方法或外部函數時。
固定并不會導致應用程序不正確,但它可能會妨礙應用程序的可伸縮性。如果虛擬線程在固定時執行阻塞操作(如 I/O 或 BlockingQueue.take () ) ,那么它的載體和底層操作系統線程將在操作期間被阻塞。長時間的頻繁固定會通過捕獲運營商而損害應用程序的可伸縮性。
調度程序不會通過擴展其并行性來補償固定。相反,可以通過修改頻繁運行的同步塊或方法來避免頻繁和長時間的固定,并保護潛在的長 I/O 操作來使用 java.util.concurrent.locks.ReentrantLock。不需要替換不常使用的同步塊和方法(例如,只在啟動時執行)或保護內存操作的同步塊和方法。一如既往,努力保持鎖定策略的簡單明了。
新的診斷有助于將代碼遷移到虛擬線程,以及評估是否應該使用 java.util.concurrent lock 替換同步的特定用法:
當線程在固定時阻塞時,會發出 JDK JFR事件。
當線程在固定時阻塞時,系統屬性 jdk.tracePinnedThreads 觸發堆棧跟蹤。使用-Djdk.tracePinnedThreads = full 運行會在線程被固定時打印一個完整的堆棧跟蹤,并突出顯示保存監視器的本機框架和框架。使用-Djdk.tracePinnedThreads = short 將輸出限制為有問題的幀。
內存使用和垃圾回收
虛擬線程的堆棧作為堆棧塊對象存儲在 Java 的垃圾回收堆中。堆棧隨著應用程序的運行而增長和縮小,這既是為了提高內存效率,也是為了容納任意深度的堆棧(直到 JVM 配置的平臺線程堆棧大小)。這種效率支持大量的虛擬線程,因此服務器應用程序中每個請求一個線程的風格可以繼續存在。
在上面的第二個例子中,回想一下,一個假設的框架通過創建一個新的虛擬線程并調用 handle 方法來處理每個請求; 即使它在深度調用堆棧的末尾調用 handle (在身份驗證、事務處理等之后) ,handle 本身也會產生多個虛擬線程,這些虛擬線程只執行短暫的任務。因此,對于每個具有深層調用堆棧的虛擬線程,都會有多個具有淺層調用堆棧的虛擬線程,這些虛擬線程消耗的內存很少。
通常,虛擬線程所需的堆空間和垃圾收集器活動的數量很難與異步代碼的數量相比較。一百萬個虛擬線程至少需要一百萬個對象,但是共享一個平臺線程池的一百萬個任務也需要一百萬個對象。此外,處理請求的應用程序代碼通常跨 I/O 操作維護數據。每個請求一個線程的代碼可以將這些數據保存在本地變量中:
這些本地變量存儲在堆中的虛擬線程堆棧中
異步代碼必須將這些數據保存在從管道的一個階段傳遞到下一個階段的堆對象中
一方面,虛擬線程需要的堆棧幀布局比緊湊對象更浪費; 另一方面,虛擬線程可以在許多情況下變異和重用它們的堆棧(取決于低級 GC 交互) ,而異步管道總是需要分配新對象,因此虛擬線程可能需要更少的分配。
總的來說,每個請求線程與異步代碼的堆消耗和垃圾收集器活動應該大致相似。隨著時間的推移,我們希望使虛擬線程堆棧的內部表示更加緊湊。
與平臺線程堆棧不同,虛擬線程堆棧不是 GC 根,所以它們中包含的引用不會被執行并發堆掃描的垃圾收集器(比如 G1)在 stop-the-world 暫停中遍歷。這也意味著,如果一個虛擬線程被阻塞,例如 BlockingQueue.take () ,并且沒有其他線程可以獲得對虛擬線程或隊列的引用,那么線程就可以被垃圾收集ーー這很好,因為虛擬線程永遠不會被中斷或解除阻塞。當然,如果虛擬線程正在運行,或者它被阻塞并且可能被解除阻塞,那么它將不會被垃圾收集。
當前虛擬線程的一個限制是 G1 GC 不支持大型堆棧塊對象。如果虛擬線程的堆棧達到區域大小的一半(可能小到512KB) ,那么可能會拋出 StackOverfloError。
具體變化
java.lang.Thread
Thread.Builder, Thread.ofVirtual(), 和 Thread.ofPlatform() 是創建虛擬線程和平臺線程的新 API,例如:
Thread thread = Thread.ofVirtual().name("duke").unstarted(runnable);
創建一個新的未啟動的虛擬線程“ duke”。
Thread.startVirtualThread(Runnable) 是創建然后啟動虛擬線程的一種方便的方法。
Thread.Builder 可以創建線程或 ThreadFactory, 后者可以創建具有相同屬性的多個線程。
Thread.isVirtual() 測試是否一個線程是一個虛擬的線程。
Thread.join 和 Thread.sleep 的新重載接受等待和睡眠時間作為java.time.Duration的實例。
新的 final 方法 Thread.threadId() 返回線程的標識符。現在不推薦使用現有的非 final 方法 Thread.getId() 。
Thread.getAllStackTraces() 現在返回所有平臺線程的映射,而不是所有線程的映射。
java.lang.Thread API其他方面沒有改變。構造器也無新變化。
虛擬線程和平臺線程之間的主要 API 差異是:
公共線程構造函數不能創建虛擬線程。
虛擬線程始終是守護進程線程,Thread.setDaemon (boolean)方法不能將虛擬線程更改為非守護進程線程。
虛擬線程有一個固定的 Thread.NORM_PRIORITY 優先級。Thread.setPriority(int)方法對虛擬線程沒有影響。在將來的版本中可能會重新討論這個限制。
虛擬線程不是線程組的活動成員。在虛擬線程上調用時,Thread.getThreadGroup() 返回一個名為“ VirtualThreads”的占位符線程組。The Thread.Builder API 不定義設置虛擬線程的線程組的方法。
使用 SecurityManager 集運行時,虛擬線程沒有權限。
虛擬線程不支持 stop(), suspend(), 或 resume()方法。這些方法在虛擬線程上調用時引發異常。
Thread-local variables
虛擬線程支持線程局部變量(ThreadLocal)和可繼承的線程局部變量(InheritableThreadLocal) ,就像平臺線程一樣,因此它們可以運行使用線程局部變量的現有代碼。但是,由于虛擬線程可能非常多,所以應該在仔細考慮之后使用線程局部變量。
特別是,不要使用線程局部變量在線程池中共享同一線程的多個任務之間共享昂貴的資源。虛擬線程永遠不應該被共享,因為每個線程在其生存期內只能運行一個任務。我們已經從 java.base 模塊中移除了許多線程局部變量的使用,以便為虛擬線程做準備,從而減少在使用數百萬個線程運行時的內存占用。
此外:
The Thread.Builder API 定義了一個在創建線程時選擇不使用線程局部變量的方法(a method to opt-out of thread locals when creating a thread)。它還定義了一個方法來選擇不繼承可繼承線程局部變量的初始值( a method to opt-out of inheriting the initial value of inheritable thread-locals)。當從不支持線程局部變量的線程調用時, ThreadLocal.get()返回初始值,ThreadLocal.set(T) 拋出異常。
遺留上下文類加載器( context class loader)現在被指定為像可繼承的線程本地一樣工作。如果在不支持線程局部變量的線程上調用 Thread.setContextClassLoader(ClassLoader),那么它將引發異常。
Networking
網絡 API 在java.net 和java.nio.channels 包中的實現現在與虛擬線程一起工作: 虛擬線程上的一個操作阻塞,例如,建立網絡連接或從套接字讀取,釋放底層平臺線程來做其他工作。
為了允許中斷和取消, java.net.Socket定義的阻塞 I/O 方法、ServerSocket 和 DatagramSocket 現在被指定為在虛擬線程中調用時是可中斷的: 中斷套接字上被阻塞的虛擬線程將釋放線程并關閉套接字。
當從 InterruptibleChannel 獲取時,這些類型套接字上的阻塞 I/O 操作總是可中斷的,因此這種更改使這些 API 在創建時的行為與從通道獲取時的構造函數的行為保持一致。
java.io
The java.io 包為字節和字符流提供 API。這些 API 的實現是高度同步的,需要進行更改以避免在虛擬線程中使用被固定。
在底層中,面向字節的輸入/輸出流沒有指定為線程安全的,也沒有指定在讀或寫方法中阻塞線程時調用 close() 時的預期行為。在大多數情況下,使用來自多個并發線程的特定輸入或輸出流是沒有意義的。面向字符的讀/寫器也沒有被指定為線程安全的,但是它們確實為子類公開了一個鎖對象。除了固定外,這些類中的同步還存在問題和不一致; 例如, InputStreamReader 和 OutputStreamWriter 使用的流解碼器和編碼器在流對象而不是鎖對象上進行同步。
為了防止固定,現在的實現如下:
BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter, PrintStream, 和 PrintWriter 現在在直接使用時使用顯式鎖而不是監視器。當這些類被子類化時,它們與以前一樣進行同步。
InputStreamReader 和 OutputStreamWriter 使用的流解碼器和編碼器現在使用與封閉的 InputStreamReader 或 OutputStreamWriter 相同的鎖。
更進一步并消除所有這些常常不必要的鎖定超出了本文的范圍。
此外,BufferedOutputStream、 BufferedWriter 和 OutputStreamWriter 的流編碼器使用的緩沖區的初始大小現在更小了,以便在堆中有許多流或寫入器時減少內存使用ーー如果有一百萬個虛擬線程,每個線程在套接字連接上都有一個緩沖流,就可能出現這種情況。
以上就是關于“Java之JDK19虛擬線程的知識點有哪些”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。