在 Spring 6 中使用虛擬線程
在這個簡短的教程中,我們將了解如何在 Spring Boot 應(yīng)用程序中利用虛擬線程的強(qiáng)大功能。
虛擬線程是Java 19 的
,這意味著它們將在未來 12 個月內(nèi)包含在官方 JDK 版本中。 最初由 Project Loom 引入,為開發(fā)人員提供了開始嘗試這一出色功能的選項(xiàng)。首先,我們將看到“平臺線程”和“虛擬線程”之間的主要區(qū)別。接下來,我們將使用虛擬線程從頭開始構(gòu)建一個 Spring-Boot 應(yīng)用程序。最后,我們將創(chuàng)建一個小型測試套件,以查看簡單 Web 應(yīng)用程序吞吐量的最終改進(jìn)。
二、 虛擬線程與平臺線程
主要區(qū)別在于
在其操作周期中不依賴于操作系統(tǒng)線程:它們與硬件解耦,因此有了“虛擬”這個詞。這種解耦是由 JVM 提供的抽象層實(shí)現(xiàn)的。對于本教程來說,必須了解虛擬線程的運(yùn)行成本遠(yuǎn)低于平臺線程。它們消耗的分配內(nèi)存量要少得多。這就是為什么可以創(chuàng)建數(shù)百萬個虛擬線程而不會出現(xiàn)內(nèi)存不足問題,而不是使用標(biāo)準(zhǔn)平臺(或內(nèi)核)線程創(chuàng)建幾百個虛擬線程。
從理論上講,這賦予了開發(fā)人員一種超能力:無需依賴異步代碼即可管理高度可擴(kuò)展的應(yīng)用程序。
三、在Spring 6中使用虛擬線程
從 Spring Framework 6(和 Spring Boot 3)開始,虛擬線程功能正式公開,但虛擬線程是Java 19 的代碼:
這意味著我們需要告訴 JVM 我們要在應(yīng)用程序中啟用它們。由于我們使用 Maven 來構(gòu)建應(yīng)用程序,因此我們希望確保在 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將取代標(biāo)準(zhǔn)的ApplicationTaskExecutor ,提供為每個任務(wù)啟動新虛擬線程的Executor。第二個 bean,名為ProtocolHandlerVirtualThreadExecutorCustomizer,將以相同的方式 自定義標(biāo)準(zhǔn)TomcatProtocolHandler 。我們還添加了注釋文件中配置屬性的值來按需啟用虛擬線程: **以通過切換application.yaml
spring:
? ?thread-executor: virtual
? ?//...
我們來測試一下Spring Boot應(yīng)用程序是否使用虛擬線程來處理Web請求調(diào)用。為此,我們需要構(gòu)建一個簡單的控制器來返回所需的信息:
@RestController
@RequestMapping("/thread")
public class ThreadController {
? ?@GetMapping("/name")
? ?public String getThreadName() {
? ? ? ?return Thread.currentThread().toString();
? ?}
}
Thread對象的toString ()方法將返回我們需要的所有信息:線程 ID、線程名稱、線程組和優(yōu)先級。讓我們通過一個 請求來訪問這個端點(diǎn):
$ curl -s http://localhost:8080/thread/name
$ VirtualThread[#171]/runnable@ForkJoinPool-1-worker-4
正如我們所看到的,響應(yīng)明確表示我們正在使用虛擬線程來處理此 Web 請求。換句話說,Thread.currentThread()調(diào)用返回虛擬線程類的實(shí)例?,F(xiàn)在讓我們通過簡單但有效的負(fù)載測試來看看虛擬線程的有效性。
四、性能比較
對于此負(fù)載測試,我們將使用
。這不是虛擬線程和標(biāo)準(zhǔn)線程之間的完整性能比較,而是我們可以使用不同參數(shù)構(gòu)建其他測試的起點(diǎn)。在這種特殊的場景中,我們將調(diào)用Rest Controller中的一個端點(diǎn),該端點(diǎn)將簡單地讓執(zhí)行休眠一秒鐘,模擬復(fù)雜的異步任務(wù):
@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 中變量的值即可在虛擬線程和標(biāo)準(zhǔn)線程之間切換。
JMeter 測試將僅包含一個線程組,模擬 1000 個并發(fā)用戶訪問/load 端點(diǎn) 100 秒:


發(fā)生這種情況是因?yàn)槠脚_線程是一種有限的資源,當(dāng)所有計劃的和池化的線程都忙時,Spring 應(yīng)用程序除了將請求擱置直到一個線程空閑之外別無選擇。
讓我們看看虛擬線程會發(fā)生什么:

正如我們所看到的,響應(yīng)穩(wěn)定在 1000 毫秒。虛擬線程在請求后立即創(chuàng)建和使用,因?yàn)閺馁Y源的角度來看它們非常便宜。在本例中,我們正在比較 spring 默認(rèn)固定標(biāo)準(zhǔn)線程池(默認(rèn)為 200)和 spring 默認(rèn)無界虛擬線程池的使用情況。
這種性能提升之所以可能,是因?yàn)閳鼍斑^于簡單,并且沒有考慮 Spring Boot 應(yīng)用程序可以執(zhí)行的全部操作。