擁抱虛擬執行緒

工程 | Mark Paluch | 2022 年 10 月 11 日 | ...

Project Loom 已透過 JEP 425 進入 JDK。自 2022 年 9 月 Java 19 釋出以來,它作為一個預覽特性提供。其目標是大幅減少編寫、維護和觀察高吞吐量併發應用程式的工作量。

虛擬執行緒的適用場景

這使得輕量級的虛擬執行緒成為應用開發者和 Spring Framework 的一個令人興奮的方法。過去幾年,應用程式之間透過網路相互通訊的趨勢日益明顯。許多應用程式利用資料儲存、訊息代理和遠端服務。如果 I/O 密集型應用程式是構建來使用阻塞式 I/O 功能(如 InputStream 和同步的 HTTP、資料庫以及訊息代理客戶端)的,那麼它們是受益於虛擬執行緒的主要應用。在虛擬執行緒上執行此類工作負載有助於減少記憶體佔用,並且在某些情況下,虛擬執行緒可以增加併發性。

如果系統具有必要的額外資源來支援併發,則可以實現更高的併發性。具體來說,這些資源包括:

  1. 連線池中可用的連線

  2. 足以應對增加負載的記憶體

  3. 未使用的 CPU 時間

虛擬執行緒的使用顯然不限於直接減少記憶體佔用或增加併發性。虛擬執行緒的引入還促使人們對執行時在僅有平臺執行緒可用時做出的決策進行更廣泛的重新審視。

併發工具的修訂

如果未配置 ThreadFactory,Spring Framework 的 SimpleAsyncTaskExecutor 會為提交的每個 Runnable 建立一個新的平臺執行緒。這種安排需要建立平臺執行緒,從而導致吞吐量降低和記憶體消耗增加。可以修訂 SimpleAsyncTaskExecutor 以在預設配置下使用虛擬執行緒,從而減少記憶體佔用並提高吞吐量。(同時,可以使用自定義的 TaskExecutor 變體來實現同樣的效果。)

程式設計模型的修訂

虛擬執行緒可以改變我們對非同步程式設計介面的看法。如果我們假設程式碼執行在虛擬執行緒上,那麼在許多情況下,使用非同步程式設計模型的原因就不復存在了。虛擬執行緒的分配非常輕量級,執行緒數量不再是可伸縮性的主要限制。更清楚地說,非同步程式設計模型並不能消除網路呼叫等操作的延遲。非同步的 Apache HTTP Client 或 Netty 在網路呼叫無法繼續時只是切換任務,而不是阻塞執行緒。對於虛擬執行緒也是如此:它們會有效地讓出給另一個可以繼續工作的 Runnable

Project Loom 重新審視了 Java 執行時庫中所有可能阻塞的區域,並更新了程式碼,以便在遇到阻塞時讓出執行權。Java 的併發工具(例如 ReentrantLockCountDownLatchCompletableFuture)可以在虛擬執行緒上使用,而不會阻塞底層的平臺執行緒。這一改變使得 Future.get().get(Long, TimeUnit) 在虛擬執行緒上成為“好公民”,並消除了使用 Future 的回撥驅動方式的需求。

非同步 Servlet API 賴以建立的假設可能會因虛擬執行緒的引入而失效。引入非同步 Servlet API 的目的是釋放伺服器執行緒,以便伺服器在工作執行緒處理請求時能夠繼續處理其他請求。在虛擬執行緒上執行 Servlet 請求和響應處理消除了釋放伺服器執行緒的需求,從而引出了一個問題:為什麼還要使用 ServletRequest.startAsync()?因為非同步分支涉及大量狀態儲存,而這些狀態可能不再需要,因此可以消除。

緩解侷限性

自虛擬執行緒被稱為 Fibers 以來,我們的團隊一直在進行實驗。從那時起直到 Java 19 釋出,一個突出的侷限性是使用 synchronized 時會導致平臺執行緒固定(pinning),從而有效降低併發性。使用 synchronized 程式碼塊本身並不是問題;問題僅在於這些程式碼塊中包含阻塞程式碼,通常指的是 I/O 操作。這種安排可能存在問題,因為載體平臺執行緒是有限的資源,在虛擬執行緒上執行程式碼而未仔細檢查工作負載時,平臺執行緒固定可能導致應用程式效能下降。實際上,即使沒有虛擬執行緒,同步塊中的相同阻塞程式碼也可能導致效能問題。

Spring Framework 大量使用 synchronized 來實現鎖定,主要圍繞本地資料結構。多年來,在虛擬執行緒可用之前,我們已經修訂了可能與第三方資源互動的 synchronized 塊,消除了高併發應用程式中的鎖爭用。因此,憑藉其龐大的社群和來自現有併發應用程式的廣泛反饋,Spring 的現狀已經相當不錯。為了在虛擬執行緒場景中成為最佳公民,我們將進一步審視 synchronized 在 I/O 或其他阻塞程式碼上下文中的使用,以避免在熱點程式碼路徑中發生平臺執行緒固定,從而使您的應用程式能夠最大程度地利用 Project Loom。

在虛擬執行緒上執行 Spring 應用程式

藉助最新版本的 Spring Framework、Spring Boot 和 Apache Tomcat,您可以自行開始實驗。您可以開始分析虛擬執行緒如何影響您的應用程式工作負載,並對虛擬執行緒的使用與平臺執行緒的使用進行基準測試。要自定義您的 Spring Boot 應用程式,使其在虛擬執行緒上處理 Servlet 請求,請應用以下配置:

@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
public AsyncTaskExecutor asyncTaskExecutor() {
  return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}

@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
  return protocolHandler -> {
    protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
  };
}

我們正在盡一切努力使預覽體驗儘可能順暢,並且我們希望在 Project Loom 在新的 OpenJDK 版本中脫離預覽狀態後提供一流的配置選項。

如果我們發現核心框架中存在虛擬執行緒最佳化(無論是某些 synchronized 使用點還是某些 ThreadLocal 使用)的具體潛力,我們將盡最大可能在即將釋出的 Spring Framework 和 Spring Boot 維護版本中進行相應的改進,甚至在 Project Loom 全面可用之前。

虛擬執行緒不僅影響 Spring Framework,還影響所有相關的整合,例如資料庫驅動程式、訊息系統、HTTP 客戶端等等。許多這些專案都意識到需要改進它們的 synchronized 行為,以充分發揮 Project Loom 的潛力。

您的應用程式會從虛擬執行緒中受益嗎?

這是一個比“是否會有益處”更具體、也更難回答的問題。

我們可以說,最有可能讓您幾乎不做任何更改就能受益的場景是,如果您目前完全沒有進行任何非同步操作(即使是 Servlet 3.1 風格的非同步請求也沒有,否則您可能需要進行一些修訂以更好地對齊)。當然,還需要有一些實際的 I/O 或其他執行緒掛起操作,Project Loom 才能帶來好處。

我們也相信 ReactiveX 風格的 API 仍然是組合併發邏輯的強大方式,也是處理流的自然方式。我們將虛擬執行緒視為對響應式程式設計模型的補充,它們消除了阻塞式 I/O 的障礙,而僅使用虛擬執行緒處理無限流仍然是一個挑戰。ReactiveX 是適用於宣告性併發(如 Scatter-Gather)場景的正確方法。底層的 Reactive Streams 規範定義了一個關於資料管道需求、背壓和取消的協議,而不侷限於非阻塞 API 或特定的執行緒使用。

我們非常期待從各種應用程式中獲得的集體經驗和反饋。我們目前的重點是確保您能夠開始自己的實驗。如果您在早期的虛擬執行緒實驗中遇到特定問題,請向相應的專案報告。

請在您的 Spring 應用程式中嘗試使用虛擬執行緒,並告訴我們結果如何!

獲取 Spring Newsletter

透過 Spring Newsletter 保持連線

訂閱

領先一步

VMware 提供培訓和認證,助您飛速發展。

瞭解更多

獲取支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將到來的活動

檢視 Spring 社群所有即將到來的活動。

檢視全部