Java的虛擬執行緒(協程)特性開啟預覽階段,多執行緒開發的難度將大大降低

碼農小胖哥發表於2022-04-08

高併發、多執行緒一直是Java程式設計中的難點,也是面試題中的要點。Java開發者也一直在嘗試使用多執行緒來解決應用伺服器的併發問題。但是多執行緒並不容易,為此一個新的技術出現了,這就是虛擬執行緒。

傳統多執行緒的痛點

但是編寫多執行緒程式碼是非常不容易的,難以控制的執行順序,共享變數的執行緒安全性,異常的可觀察性等等都是多執行緒程式設計的難點。

如果每個請求在請求的持續時間內都在一個執行緒中處理,那麼為了提高應用程式的吞吐量,執行緒的數量必須隨著吞吐量的增長而增長。不幸的是執行緒是稀缺資源,建立一個執行緒的代價是昂貴的,即使引入了池化技術也無法降低新執行緒的建立成本,而且 JDK 當前的執行緒實現將應用程式的吞吐量限制在遠低於硬體可以支援的水平。

為此很多開發人員轉向了非同步程式設計,例如CompletableFuture或者現在正熱的反應式框架。但是這些技術要麼擺脫不了“回撥地獄”,要麼缺乏可觀測性。

解決這些痛點、增強Java平臺的和諧,實現每個請求使用獨立執行緒(thread-per-request style)這種風格成為必要之舉。能否實現一種“成本低廉”的虛擬執行緒來對映到系統執行緒以減少對系統執行緒的直接操作呢?思路應該是沒問題的!於是Java社群發起了關於虛擬執行緒的JEP 425提案。

虛擬執行緒

虛擬執行緒(virtual threads)應該非常廉價而且可以無需擔心繫統硬體資源被大量建立,並且不應該被池化。應該為每個應用程式任務建立一個新的虛擬執行緒。因此,大多數虛擬執行緒將是短暫的並且具有淺層呼叫堆疊,只執行單個任務 HTTP 客戶端呼叫或單個 JDBC 查詢。與之對應的平臺執行緒( Platform Threads,也就是現在傳統的JVM執行緒 )是重量級且昂貴的,因此通常必須被池化。它們往往壽命長,有很深的呼叫堆疊,並且在許多工之間共享。

總而言之,虛擬執行緒保留了與 Java 平臺的設計相協調的、可靠的獨立請求執行緒(thread-per-request style),同時優化了硬體的利用。使用虛擬執行緒不需要學習新概念,甚至需要改掉現在操作多執行緒的習慣,使用更加容易上手的API、相容以前的多執行緒設計、並且絲毫不會影響程式碼的擴充性。

平臺執行緒和虛擬執行緒的不同

為了更好理解這一個設計,草案對這兩種執行緒進行了比較。

現在的執行緒

現在每個java.lang.Thread都是一個平臺執行緒,平臺執行緒在底層作業系統執行緒上執行 Java 程式碼,並在程式碼的整個生命週期內捕獲作業系統執行緒。平臺執行緒數受限於 OS 執行緒數。

平臺執行緒並不會因為加入虛擬執行緒而退出歷史舞臺。

未來的虛擬執行緒

虛擬執行緒是由 JDK 而不是作業系統提供的執行緒的輕量級實現。它們是使用者模式執行緒的一種形式,在其他多執行緒語言中已經成功(比如Golang中的協程和Erlang中的程式)。 虛擬執行緒採用 M:N 排程,其中大量 (M) 虛擬執行緒被排程為在較少數量 (N) 的 OS 執行緒上執行。 JDK 的虛擬執行緒排程程式是一種ForkJoinPool工作竊取的機制,以 FIFO 模式執行。

我們可以很隨意地建立10000個虛擬執行緒:

// 預覽程式碼
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
}  

無需擔心硬體資源是否扛得住,反過來如果你使用 Executors.newCachedThreadPool() 建立10000個平臺執行緒,在大多數作業系統上很容易因資源不足而崩潰。

為吞吐量而設計

但是這裡依然要說明一點,虛擬執行緒並是為了提升執行速度而設計。它並不比平臺執行緒速度快,它們的存在是為了提供規模(更高的吞吐量),而不是速度(更低的延遲)。它們的數量可能比平臺執行緒多得多,因此根據利特爾定律,它們可以實現更高吞吐量所需的更高併發性。

換句話說,虛擬執行緒可以顯著提高應用程式吞吐量

  • 併發任務的數量很高(超過幾千個),並且
  • 工作負載不受 CPU 限制,因為在這種情況下,擁有比處理器核心多得多的執行緒並不能提高吞吐量。

虛擬執行緒有助於提高傳統伺服器應用程式的吞吐量,正是因為此類應用程式包含大量併發任務,這些任務花費大量的時間等待。

增強可觀測性

編寫清晰的程式碼並不是全部。對正在執行的程式狀態的清晰表示對於故障排除、維護和優化也很重要,JDK 長期以來一直提供除錯、分析和監視執行緒的機制。 在虛擬執行緒中也會增強程式碼的可觀測性,讓開發人員更好地除錯程式碼。

新的執行緒API

為此增加了新的執行緒API設計,目前放出的部分如下:

  • Thread.Builder 執行緒構建器。
  • ThreadFactory 能批量構建相同特性的執行緒工廠。
  • Thread.ofVirtual() 建立一個虛擬執行緒。
  • Thread.ofPlatform() 建立一個平臺執行緒。
  • Thread.startVirtualThread(Runnable) 一種建立然後啟動虛擬執行緒的便捷方式。
  • Thread.isVirtual() 測試執行緒是否是虛擬執行緒。
還有很多就不一一演示了,有興趣的自行去看JEP425

總結

JEP425還有很多的細節,基於我個人理解能力的不足只能解讀這麼多了。協程在Java社群已經呼喚了很久了,現在終於有了實質性的動作,這是一個令人振奮的好訊息。不過這個功能涉及的東西還是很多的,包括平臺執行緒的相容性、對ThreadLocal的一些影響、對JUC的影響。可能需要多次預覽才能最終落地。胖哥可能趕不上那個時候了,不過很多年輕的同學應該能夠趕上。

關注公眾號:Felordcn 獲取更多資訊

個人部落格:https://felord.cn

相關文章