在 Spring 6 中使用虛擬執行緒

2023-08-27 21:00:27

一、簡介

在這個簡短的教學中,我們將瞭解如何在 Spring Boot 應用程式中利用虛擬執行緒的強大功能。

虛擬執行緒是Java 19 的預覽功能,這意味著它們將在未來 12 個月內包含在官方 JDK 版本中。Spring 6 版本最初由 Project Loom 引入,為開發人員提供了開始嘗試這一出色功能的選項。

首先,我們將看到「平臺執行緒」和「虛擬執行緒」之間的主要區別。接下來,我們將使用虛擬執行緒從頭開始構建一個 Spring-Boot 應用程式。最後,我們將建立一個小型測試套件,以檢視簡單 Web 應用程式吞吐量的最終改進。

二、 虛擬執行緒與平臺執行緒

主要區別在於虛擬執行緒在其操作週期中不依賴於作業系統執行緒:它們與硬體解耦,因此有了「虛擬」這個詞。這種解耦是由 JVM 提供的抽象層實現的。

對於本教學來說,必須瞭解虛擬執行緒的執行成本遠低於平臺執行緒。它們消耗的分配記憶體量要少得多。這就是為什麼可以建立數百萬個虛擬執行緒而不會出現記憶體不足問題,而不是使用標準平臺(或核心)執行緒建立幾百個虛擬執行緒。

從理論上講,這賦予了開發人員一種超能力:無需依賴非同步程式碼即可管理高度可延伸的應用程式。

三、在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 ,提供為每個任務啟動新虛擬執行緒的Executor。第二個 bean,名為ProtocolHandlerVirtualThreadExecutorCustomizer,將以相同的方式 自定義標準TomcatProtocolHandler 。我們還新增了註釋@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物件的toString ()方法將返回我們需要的所有資訊:執行緒 ID、執行緒名稱、執行緒組和優先順序。讓我們通過一個curl請求來存取這個端點:

$ curl -s http://localhost:8080/thread/name
$ VirtualThread[#171]/runnable@ForkJoinPool-1-worker-4

正如我們所看到的,響應明確表示我們正在使用虛擬執行緒來處理此 Web 請求。換句話說,Thread.currentThread()呼叫返回虛擬執行緒類的範例。現在讓我們通過簡單但有效的負載測試來看看虛擬執行緒的有效性。

四、效能比較

對於此負載測試,我們將使用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 應用程式除了將請求擱置直到一個執行緒空閒之外別無選擇。

讓我們看看虛擬執行緒會發生什麼:

正如我們所看到的,響應穩定在 1000 毫秒。虛擬執行緒在請求後立即建立和使用,因為從資源的角度來看它們非常便宜。在本例中,我們正在比較 spring 預設固定標準執行緒池(預設為 200)和 spring 預設無界虛擬執行緒池的使用情況。

這種效能提升之所以可能,是因為場景過於簡單,並且沒有考慮 Spring Boot 應用程式可以執行的全部操作。從底層作業系統基礎設施中採用這種抽象可能是有好處的,但並非在所有情況下都是如此。