虛擬線程在Spring Boot中的應(yīng)用及性能對比
環(huán)境:Spring Boot3.2.5
1. 簡介
在本篇文章中,我們將學習如何在Spring Boot應(yīng)用程序中利用虛擬線程的強大功能。
虛擬線程由Project Loom引入,并在Java 19中作為預覽功能提供,并且在成為官方JDK 21版本的一部分。此外,Spring 6版本集成了這一強大功能,允許開發(fā)者進行嘗試。
首先,我們將了解“平臺線程”與“虛擬線程”之間的主要區(qū)別。接下來,我們將從頭開始構(gòu)建一個使用虛擬線程的Spring Boot應(yīng)用程序。最后,我們將創(chuàng)建一個小型測試,以檢查簡單Web應(yīng)用的吞吐量是否有所提升。
虛擬線程 VS. 平臺線程
主要區(qū)別在于,虛擬線程在運行周期內(nèi)不依賴操作系統(tǒng)線程。虛擬線程與硬件解耦,因此稱為 "虛擬"。此外,JVM 提供的抽象層賦予了這種解耦。
在本文中,我們要驗證虛擬線程的運行成本遠低于平臺線程。我們要確認,創(chuàng)建數(shù)百萬個虛擬線程不會出現(xiàn)內(nèi)存不足錯誤(平臺線程容易出現(xiàn)此問題)。
關(guān)于虛擬線程的詳細介紹,可查看下面這篇文章
提升系統(tǒng)吞吐量,詳解JDK21虛擬線程,炸裂
2. 實戰(zhàn)案例
2.1 開啟虛擬線程支持
從 Spring Boot 3.2 開始,如果我們使用 Java 21,啟用虛擬線程非常簡單。我們將 spring.threads.virtual.enabled 屬性設(shè)置為 true,然后就可以開始了:
spring:
threads:
virtual:
enabled: true
理論上,我們不需要做其他任何事情。但是,從普通線程切換到虛擬線程可能會給傳統(tǒng)應(yīng)用程序帶來不可預見的后果。因此,我們必須對應(yīng)用程序進行全面測試。
2.2 驗證虛擬線程
通過上面開啟虛擬線程后,我們通過如下方式是否正確的開啟了虛擬線程。
@GetMapping("name")
public String toThread() {
return Thread.currentThread().toString() ;
}
這里我們打印當前處理請求的線程名稱,輸出結(jié)果:
圖片
響應(yīng)結(jié)果明確指出我們正在使用虛擬線程處理此網(wǎng)絡(luò)請求。換句話說,Thread.currentThread() 調(diào)用返回了 VirtualThread 類的一個實例。
2.3 性能對比
為了比較性能,我們將使用 JMeter 運行負載測試。這并不是一個完整的性能比較,而是一個起點,我們可以從這個起點出發(fā),用不同的參數(shù)建立更多的測試。
在這個特定場景中,我們將通過Controller接口進行測試,該接口只需讓執(zhí)行進入休眠狀態(tài)一秒鐘,模擬一個復雜的異步任務(wù):
@RestController
@RequestMapping("/load")
public class LoadTestController {
private static final Logger logger = LoggerFactory.getLogger(LoadTestController.class) ;
@GetMapping
public void test() throws InterruptedException {
logger.info("日志信息...") ;
// 模擬耗時操作
Thread.sleep(1000) ;
}
}
接下來,在JMeter中創(chuàng)建一個線程組,模擬 1000 個并發(fā)用戶在 100 秒內(nèi)訪問 /load 接口:
圖片
在這種情況下,采用這項新功能所帶來的性能提升是顯而易見的。讓我們比較一下不同實現(xiàn)的 "響應(yīng)時間圖"。這是標準線程的響應(yīng)時間圖。我們可以看到,完成一次調(diào)用所需的時間很快就達到了 5000 毫秒:
圖片
這種情況發(fā)生是因為平臺線程是一種有限資源。當所有計劃的和池中的線程都在忙碌時,Spring 應(yīng)用程序只能等待,直到有一個線程空閑下來,才能處理該請求。
接下來,使用虛擬線程進行測試
圖片
生成的圖表顯示,響應(yīng)時間穩(wěn)定在1000毫秒。因此,從資源消耗的角度來看,虛擬線程非常高效,請求發(fā)出后會立即創(chuàng)建并使用它們。
這種性能提升僅在像我們的演示示例這樣的簡單場景中才可能實現(xiàn)。實際上,對于CPU密集型操作,虛擬線程并不合適,因為這類任務(wù)需要極少的阻塞。
下面,我們在通過一個需要CPU大量計算的操作進行測試,測試代碼如下:
// 該示例計算大數(shù)的階乘
@GetMapping("calc")
public String calc() {
// 取值越大計算耗時就越高
int number = 20000 ;
// 開始時間
long startTime = System.currentTimeMillis();
System.out.println("開始時間: " + new Date(startTime));
// 執(zhí)行耗時計算
factorial(number);
// 結(jié)束時間
long endTime = System.currentTimeMillis();
System.out.println("結(jié)束時間: " + new Date(endTime));
// 計算總耗時
long duration = (endTime - startTime);
return "計算" + number + "! 耗時: " + duration + " 毫秒" ;
}
private static BigInteger factorial(int n) {
BigInteger result = BigInteger.ONE;
for (int i = 1; i <= n; i++) {
result = result.multiply(BigInteger.valueOf(i));
}
return result;
}
首先,是平臺線程測試結(jié)果如下:
圖片
如下,是虛擬線程測試結(jié)果
圖片
根據(jù)這里的測試結(jié)果,發(fā)現(xiàn)他們的結(jié)果差不多。但虛擬線程似乎更加平穩(wěn)吧。