在 Spring 6 中使用虛擬線程
一、簡介
在這個簡短的教程中,我們將了解如何在 Spring Boot 應用程序中利用虛擬線程的強大功能。
虛擬線程是 Java 19 的預覽功能,這意味著它們將在未來 12 個月內包含在正式的 JDK 版本中。最初由 Project Loom 引入, Spring 6 版本為開發人員提供了開始試驗這一出色功能的選項。
首先,我們將了解“平台線程”和“虛擬線程”之間的主要區別。接下來,我們將使用虛擬線程從頭開始構建一個 Spring-Boot 應用程序。最後,我們將構建一個小型測試套件,以查看簡單 Web 應用程序吞吐量的最終改進。
2. 虛擬線程與平台線程
主要區別在於虛擬線程在其運行週期中不依賴操作系統線程:它們與硬件分離,因此稱為“虛擬”。這種分離是由 JVM 提供的抽象層授予的。
出於本教程的目的,重要的是要了解虛擬線程的運行成本遠低於平台線程。它們消耗的內存量更少。這就是為什麼可以創建數百萬個虛擬線程而不會出現內存不足問題,而不是我們可以使用標準平台(或內核)線程創建的幾百個。
從理論上講,這賦予了開發人員超能力:在不依賴異步代碼的情況下管理高度可擴展的應用程序。
3. 在 Spring 6 中使用虛擬線程
從 Spring Framework 6(和 Spring Boot 3)開始,虛擬線程功能正式全面可用,但虛擬線程是 Java 19 的預覽功能。這意味著我們需要告訴 JVM 我們要在應用程序中啟用它們.由於我們使用 Maven 來構建我們的應用程序,因此我們要確保在pom.xml
中包含以下代碼:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>19</source>
<target>19</target>
<compilerArgs>
--enable-preview
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
從 Java 的角度來看,要使用 Apache Tomcat 和虛擬線程,我們只需要一個帶有幾個 bean 的簡單配置類:
@EnableAsync
@Configuration
@ConditionalOnProperty(
value = "spring.thread-executor",
havingValue = "virtual"
)
public class ThreadConfig {
@Bean
public AsyncTaskExecutor applicationTaskExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
}
第一個 Spring Bean,名為ApplicationTaskExecutor
,將取代標準的[ApplicationTaskExecutor](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.html)
提供一個Executor
為每個任務啟動一個新的虛擬線程。第二個 bean 名為ProtocolHandlerVirtualThreadExecutorCustomizer,
將以相同的方式自定義標準[TomcatProtocolHandler](https://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/coyote/ProtocolHandler.html)
。我們還添加了註釋@ConditionalOnProperty
以通過切換application.yaml
文件中配置屬性的值來按需啟用虛擬線程:
spring:
thread-executor: virtual
//...
現在讓我們測試 Spring Boot 應用程序是否使用虛擬線程來處理 Web 請求調用。為此,我們需要構建一個簡單的控制器來返回所需的信息:
@RestController
@RequestMapping("/thread")
public class ThreadController {
@GetMapping("/name")
public String getThreadName() {
return Thread.currentThread().toString();
}
}
[Thread](https://docs.oracle.com/en/java/javase/19/docs/api/java.base/java/lang/Thread.html)
對象的toString()
方法將返回我們需要的所有信息:Thread Id、Thread Name、Thread Group 和 Priority。讓我們用curl
請求訪問這個端點:
$ curl -s http://localhost:8080/thread/name
$ VirtualThread[#171]/[email protected]
如我們所見,響應明確表示我們正在使用虛擬線程來處理此 Web 請求。換句話說, Thread.currentThread()
調用返回虛擬線程類的一個實例。現在讓我們通過一個簡單但有效的負載測試來了解虛擬線程的有效性。
4 .性能比較
對於此負載測試,我們將使用 JMeter。這不是虛擬線程和標準線程之間的完整性能比較,而是一個起點,我們可以從中構建具有不同參數的其他測試。
在這種特殊情況下,我們將在Rest Controller
中調用一個端點,它將簡單地讓執行休眠一秒鐘,模擬一個複雜的異步任務:
@RestController
@RequestMapping("/load")
public class LoadTestController {
private static final Logger LOG = LoggerFactory.getLogger(LoadTestController.class);
@GetMapping
public void doSomething() throws InterruptedException {
LOG.info("hey, I'm doing something");
Thread.sleep(1000);
}
}
請記住,由於@ConditionalOnProperty
註釋,我們可以通過僅更改application.yaml
中的變量值來在虛擬線程和標準線程之間切換。
JMeter 測試將只包含一個線程組,模擬 1000 個並髮用戶點擊/load
端點 100 秒:
在這種情況下,採用此新功能帶來的性能提升是顯而易見的。讓我們比較一下不同實現的“響應時間圖”。這是標準線程的響應圖。正如我們所看到的,立即完成調用所需的時間達到 5000 毫秒:
發生這種情況是因為平台線程是一種有限的資源,當所有調度線程和池線程都忙時,Spring App 沒有什麼可做的,只能暫停請求直到一個線程空閒。
讓我們看看虛擬線程會發生什麼:
正如我們所見,響應穩定在 1000 毫秒。虛擬線程在請求後立即創建和使用,因為從資源的角度來看它們非常便宜。
這種性能提升是可能的,因為場景很簡單,沒有考慮 Spring Boot 應用程序可以做的所有事情。從底層操作系統基礎設施中採用這種抽象可能會帶來好處,但並非在所有情況下都如此。
5.結論
在本文中,我們了解瞭如何在基於 Spring 6 的應用程序中使用虛擬線程。
與往常一樣,代碼可在 GitHub 上獲得。