擁抱虛擬執行緒

工程 | 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 時間

使用虛擬執行緒顯然不限於直接減少記憶體佔用或提高併發性。引入虛擬執行緒也促使我們重新審視在只有平臺執行緒可用時為執行時做出的決策。

併發實用工具的修訂

Spring Framework 的 SimpleAsyncTaskExecutor 在未配置 ThreadFactory 的情況下,會為每個提交的 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()?因為非同步分支涉及大量狀態儲存,而這些狀態儲存現在不再需要,可以被消除。

緩解限制

我們的團隊自虛擬執行緒被稱為“Fiber”以來一直在進行實驗。從那時起,直到 Java 19 釋出,一個普遍存在的限制導致了平臺執行緒的固定,在大量使用 synchronized 時有效降低了併發性。synchronized 程式碼塊本身不是問題;只有當這些程式碼塊包含阻塞程式碼(通常是 I/O 操作)時才會成為問題。這些情況可能很麻煩,因為承載平臺執行緒是有限的資源,在沒有仔細檢查工作負載的情況下,平臺執行緒固定在虛擬執行緒上執行程式碼可能導致應用程式效能下降。事實上,即使沒有虛擬執行緒,同步塊中相同的阻塞程式碼也可能導致效能問題。

Spring Framework 大量使用 synchronized 來實現鎖定,主要圍繞本地資料結構。多年來,在虛擬執行緒可用之前,我們已經修訂了可能與第三方資源互動的 synchronized 塊,以消除高併發應用程式中的鎖爭用。因此,由於其龐大的社群和現有併發應用程式的廣泛反饋,Spring 已經處於非常好的狀態。在成為虛擬執行緒場景中的最佳公民的道路上,我們將進一步重新審視 I/O 或其他阻塞程式碼上下文中的 synchronized 用法,以避免在熱程式碼路徑中出現平臺執行緒固定,從而使您的應用程式能夠充分利用 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());
  };
}

目前,我們正盡一切努力使預覽體驗儘可能無縫,並期望在 Loom 在新的 OpenJDK 釋出中退出預覽後,提供一流的配置選項。

如果我們發現核心框架中存在具體的面向虛擬執行緒的最佳化潛力,無論是特定的 synchronized 用法點還是特定的 ThreadLocal 用法,我們都會在可能的情況下,在 Loom 正式可用之前,將相應的改進納入即將釋出的 Spring Framework 和 Spring Boot 維護版本中。

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

您的應用程式是否將受益於虛擬執行緒?

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

我們可以說,您幾乎不做任何更改就能獲益的最可能情況是,如果您目前根本沒有進行任何非同步操作(甚至不是 Servlet 3.1 風格的非同步請求,否則您可能需要進行一些修訂以更好地對齊)。當然,Loom 要帶來好處,必須有一些實際的 I/O 或其他執行緒停頓。

我們也相信,ReactiveX 風格的 API 仍然是組合併發邏輯的強大方法,也是處理流的自然方式。我們看到虛擬執行緒補充了響應式程式設計模型,消除了阻塞 I/O 的障礙,儘管使用虛擬執行緒純粹處理無限流仍然是一個挑戰。ReactiveX 是適用於宣告式併發(如 scatter-gather)很重要的併發場景的正確方法。底層的 Reactive Streams 規範定義了一個用於資料管道的需求、背壓和取消的協議,而沒有限制自己使用非阻塞 API 或特定的執行緒使用。

我們非常期待來自應用程式的集體經驗和反饋。我們目前的重點是確保您能夠開始自行實驗。如果您在早期使用虛擬執行緒的實驗中遇到具體問題,請將其報告給相應的專案。

嘗試在您的 Spring 應用程式中使用虛擬執行緒,並讓我們知道進展如何!

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

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

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視所有