虛擬執行緒原理及效能分析|得物技術
來源:得物技術
目錄
一、背景
二、為了提升吞吐效能,我們所做的最佳化
1. 序列模式
2. 執行緒池 +Future 非同步呼叫
3. 執行緒池 +CompletableFuture 非同步呼叫
三、一請求一執行緒的模型
四、虛擬執行緒
1. 執行緒術語定義
2. 虛擬執行緒定義
3. 虛擬執行緒建立
4. 虛擬執行緒實現原理
5. 虛擬執行緒記憶體佔用評估
6. 虛擬執行緒的侷限及使用建議
7. 虛擬執行緒適用場景
五、虛擬執行緒壓測效能分析
1. 測試流程
2. 衡量指標
3. Tomcat+普通執行緒池
4. WebFlux
5. Tomcat+虛擬執行緒池
六、總結
一
背景
JDK21 在 9 月 19 號正式釋出,帶來了較多亮點,其中虛擬執行緒備受矚目,毫不誇張的說,它改變了高吞吐程式碼的編寫方式,只需要小小的變動就可以讓目前的 IO 密集型程式的吞吐量得到提升,寫出高吞吐量的程式碼不再困難。
二
為了提升吞吐效能,我們所做的最佳化
在講虛擬執行緒之前,我們先聊聊為了提高吞吐效能,我們所做的一些最佳化方案。
序列模式
在當前的微服務架構下,處理一次使用者/上游的請求,往往需要多次呼叫下游服務、資料庫、檔案系統等,再將所有請求的資料進行處理最終的結果返回給上游。
執行緒池+Future非同步呼叫
為了解決序列呼叫的低效能問題,我們會考慮使用並行非同步呼叫的方式,最簡單的方式便是使用執行緒池 +Future 去並行呼叫。
執行緒池+CompletableFuture非同步呼叫
為了降低 CPU 的阻塞等待時間和提升資源的利用率,我們會使用CompletableFuture
對呼叫流程進行編排,降低依賴之間的阻塞。
執行緒資源浪費瓶頸始終在 IO 等待上
,導致 CPU 資源利用率較低。目前大部分服務是 IO 密集型服務,一次請求的處理耗時大部分都消耗在等待下游 RPC,資料庫查詢的 IO 等待中,此時執行緒仍然只能阻塞等待結果返回,導致 CPU 的利用率很低。執行緒數量存在限制, 為了增加併發度,我們會給執行緒池配置更大的執行緒數,但是執行緒的數量是有限制的,Java 的執行緒模型是 1:1 對映平臺執行緒的,導致 Java 執行緒建立的成本很高,不能無限增加。同時隨著 CPU 排程執行緒數的增加,會導致更嚴重的資源爭用,寶貴的 CPU 資源被損耗在上下文切換上。
三
一請求一執行緒的模型
在給出最終解決方案之前,我們先聊一聊 Web 應用中常見的一請求一執行緒的模型。
擴大服務最大執行緒數
,簡單有效,由於存在下列問題,導致平臺執行緒有最大數量限制,不能大量擴充。系統資源有限導致系統執行緒總量有限,進而導致與系統執行緒一一對應的平臺執行緒有限。 平臺執行緒的排程依賴於系統的執行緒排程程式,當平臺執行緒建立過多,會消耗大量資源用於處理執行緒上下文切換。 每個平臺執行緒都會開闢一塊大小約 1m 私有的棧空間,大量平臺執行緒會佔據大量記憶體。
垂直擴充套件,升級機器配置,水平擴充套件,增加服務節點
,也就是俗稱的升配擴容大法,效果好,也是最常見的方案,缺點是會增加成本,同時有些場景下擴容並不能 100% 解決問題。採用非同步/響應式程式設計方案
,例如 RPC NIO 非同步呼叫,WebFlux,Rx-Java 等非阻塞的基於 Ractor 模型的框架,使用事件驅動使得少量執行緒即可實現高吞吐的請求處理,擁有較好的效能與優秀的資源利用,缺點是學習成本較高相容性問題較大,編碼風格與目前的一請求一執行緒的模型差異較大,理解難度大,同時對於程式碼的除錯比較困難。
JDK21 中的虛擬執行緒可能給出了答案
, JDK 提供了與 Thread 完全一致的抽象 Virtual Thread
來應對這種經常阻塞的情況,阻塞仍然是會阻塞,但是換了阻塞的物件,由昂貴的平臺執行緒阻塞改為了成本很低的虛擬執行緒的阻塞,當程式碼呼叫到阻塞 API 例如 IO,同步,Sleep 等操作時,JVM 會自動把 Virtual Thread 從平臺執行緒上解除安裝
,平臺執行緒就會去處理下一個虛擬執行緒,透過這種方式,提升了平臺執行緒的利用率,讓平臺執行緒不再阻塞在等待上,從底層實現了少量平臺執行緒就可以處理大量請求,提高了服務吞吐和 CPU 的利用率
。四
虛擬執行緒
執行緒術語定義
作業系統執行緒(OS Thread)
:由作業系統管理,是作業系統排程的基本單位。
平臺執行緒(Platform Thread)
:Java.Lang.Thread 類的每個例項,都是一個平臺執行緒,是 Java 對作業系統執行緒的包裝,與作業系統是 1:1 對映。虛擬執行緒(Virtual Thread)
:一種輕量級,由 JVM 管理的執行緒。對應的例項 java.lang.VirtualThread 這個類。載體執行緒(Carrier Thread)
:指真正負責執行虛擬執行緒中任務的平臺執行緒。一個虛擬執行緒裝載到一個平臺執行緒之後,那麼這個平臺執行緒就被稱為虛擬執行緒的載體執行緒。虛擬執行緒定義
JDK 中 java.lang.Thread 的每個例項都是一個平臺執行緒
。平臺執行緒在底層作業系統執行緒上執行 Java 程式碼,並在程式碼的整個生命週期內獨佔作業系統執行緒,平臺執行緒例項本質是由系統核心的執行緒排程程式進行排程,並且平臺執行緒的數量受限於作業系統執行緒的數量。
虛擬執行緒建立
方法一:直接建立虛擬執行緒
Thread vt = Thread.startVirtualThread(() -> { System.out.println("hello wolrd virtual thread");});
Thread.ofVirtual().unstarted(() -> { System.out.println("hello wolrd virtual thread");});vt.start();
方法三:透過虛擬執行緒的 ThreadFactory 建立虛擬執行緒
ThreadFactory tf = Thread.ofVirtual().factory();Thread vt = tf.newThread(() -> { System.out.println("Start virtual thread..."); Thread.sleep(1000); System.out.println("End virtual thread. ");});vt.start();方法四:Executors.newVirtualThreadPer
-TaskExecutor()
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(() -> {
System.out.println("Start virtual thread...");
Thread.sleep(1000);
System.out.println("End virtual thread.");
return true;
});
虛擬執行緒實現原理
虛擬執行緒是由 Java 虛擬機器排程,而不是作業系統。虛擬執行緒佔用空間小,同時使用輕量級的任務佇列來排程虛擬執行緒,避免了執行緒間基於核心的上下文切換開銷,因此可以極大量地建立和使用。
virtual thread =continuation+scheduler+runnable
Continuation
例項中:當任務需要阻塞掛起的時候,會呼叫 Continuation 的 yield
操作進行阻塞,虛擬執行緒會從平臺執行緒解除安裝。當任務解除阻塞繼續執行的時候,呼叫 Continuation.run
會從阻塞點繼續執行。
Scheduler
也就是執行器,由它將任務提交到具體的載體執行緒池中執行。它是 java.util.concurrent.Executor 的子類。 虛擬執行緒框架提供了一個預設的 FIFO 的 ForkJoinPool 用於執行虛擬執行緒任務。
Runnable
則是真正的任務包裝器,由 Scheduler 負責提交到載體執行緒池中執行。mount(掛載)
,取消分配平臺執行緒的操作稱為 unmount(解除安裝
)
:mount 操作
:虛擬執行緒掛載到平臺執行緒,虛擬執行緒中包裝的 Continuation 堆疊幀資料會被複製到平臺執行緒的執行緒棧,這是一個從堆複製到棧的過程。unmount 操作
:虛擬執行緒從平臺執行緒解除安裝,此時虛擬執行緒的任務還沒有執行完成,所以虛擬執行緒中包裝的 Continuation 棧資料幀會會留在堆記憶體中。排程器(執行緒池)中的平臺執行緒等待處理任務。
一個虛擬執行緒被分配平臺執行緒,該平臺執行緒作為載體執行緒執行虛擬執行緒中的任務。
虛擬執行緒執行其 Continuation,Mount(掛載)平臺執行緒後,最終執行 Runnable 包裝的使用者實際任務。
虛擬執行緒任務執行完成,標記 Continuation 終結,標記虛擬執行緒為終結狀態,清空上下文,等待 GC 回收,解除掛載載體執行緒會返還到排程器(執行緒池)中等待處理下一個任務。
Continuation
的 yield
操作讓出控制權,等待虛擬執行緒重新分配載體執行緒並且執行,具體見下面的程式碼:ReentrantLock lock = new ReentrantLock(); Thread.startVirtualThread(() -> { lock.lock(); }); // 確保鎖已經被上面的虛擬執行緒持有 Thread.sleep(1000); Thread.startVirtualThread(() -> { System.out.println("first"); 會觸發Continuation的yield操作 lock.lock(); try { System.out.println("second"); } finally { lock.unlock(); } System.out.println("third"); }); Thread.sleep(Long.MAX_VALUE); }
虛擬執行緒中任務執行時候呼叫 Continuation#run() 先執行了部分任務程式碼,然後嘗試獲取鎖,該操作是阻塞操作會導致 Continuation
的yield
操作讓出控制權,如果yield
操作成功,會從載體執行緒unmount
,載體執行緒棧資料會移動到 Continuation 棧的資料幀中,儲存在堆記憶體中,虛擬執行緒任務完成,此時虛擬執行緒和 Continuation 還沒有終結和釋放,載體執行緒被釋放到執行器中等待新的任務;如果 Continuation 的 yield 操作失敗,則會對載體執行緒進行 Park 呼叫,阻塞在載體執行緒上,此時虛擬執行緒和載體執行緒同時會被阻塞,本地方法,Synchronized 修飾的同步方法都會導致 yield 失敗。
當鎖持有者釋放鎖之後,會喚醒虛擬執行緒獲取鎖,獲取鎖成功後,虛擬執行緒會重新進行 mount,讓虛擬執行緒任務再次執行,此時有可能是分配到另一個載體執行緒中執行,Continuation 棧會的資料幀會被恢復到載體執行緒棧中,然後再次呼叫 Continuation#run()
恢復任務執行。
虛擬執行緒任務執行完成,標記 Continuation 終結,標記虛擬執行緒為終結狀態,清空上下文變數,解除載體執行緒的掛載載體執行緒返還到排程器(執行緒池)中作為平臺執行緒等待處理下一個任務。
Continuation
的 yield
操作進行阻塞。當任務需要解除阻塞繼續執行的時候,則呼叫 Continuation
的 run
恢復執行。--add-exports java.base/jdk.internal.vm=ALL-UNNAMED
可以在本地執行。ContinuationScope scope = new ContinuationScope("scope");Continuation continuation = new Continuation(scope, () -> { System.out.println("before yield開始"); Continuation.yield(scope); System.out.println("after yield 結束");});System.out.println("1 run");// 第一次執行Continuation.runcontinuation.run();System.out.println("2 run");// 第二次執行Continuation.runcontinuation.run();System.out.println("Done");
Continuation
例項進行 yield
呼叫後,再次呼叫其 run
方法就可以從 yield
的呼叫之處繼續往下執行
,從而實現了程式的中斷和恢復。虛擬執行緒記憶體佔用評估
單個平臺執行緒的資源佔用:
根據 JVM 規範,預留 1 MB 執行緒棧空間。 平臺執行緒例項,會佔據 2000+ byte 資料。
Continuation 棧會佔用數百 byte 到數百 KB 記憶體空間,是作為堆疊塊物件儲存在 Java 堆中。 虛擬執行緒例項會佔據 200 - 240 byte 資料。
private static final int COUNT = 4000;
/**
* -XX:NativeMemoryTracking=detail
*
* @param args args
*/
public static void main(String[] args) throws Exception {
for (int i = 0; i < COUNT; i++) {
new Thread(() -> {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (Exception e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
Thread.sleep(Long.MAX_VALUE);
}
private static final int COUNT = 4000;
/**
* -XX:NativeMemoryTracking=detail
*
* @param args args
*/
public static void main(String[] args) throws Exception {
for (int i = 0; i < COUNT; i++) {
Thread.startVirtualThread(() -> {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (Exception e) {
e.printStackTrace();
}
});
}
Thread.sleep(Long.MAX_VALUE);
}
虛擬執行緒的侷限及使用建議
虛擬執行緒存在 native
方法或者外部方法
(Foreign Function & Memory API,jep 424 ) 呼叫不能進行yield
操作,此時載體執行緒會被阻塞。當執行在 synchronized
修飾的程式碼塊或者方法時,不能進行yield
操作,此時載體執行緒會被阻塞,推薦使用 ReentrantLock。ThreadLocal 相關問題,目前虛擬執行緒仍然是支援 ThreadLocal 的,但是由於虛擬執行緒的數量非常多,會導致 Threadlocal 中存的執行緒變數非常多,需要頻繁 GC 去清理,對效能會有影響,官方建議儘量少使用 ThreadLocal,同時不要在虛擬執行緒的 ThreadLocal 中放大物件,目前官方是想透過 ScopedLocal 去替換掉 ThreadLocal,但是在 21 版本還沒有正式釋出,這個可能是大規模使用虛擬執行緒的一大難題。 無需池化虛擬執行緒 虛擬執行緒佔用的資源很少,因此可以大量地建立而無須考慮池化,它不需要跟平臺執行緒池一樣,平臺執行緒的建立成本比較昂貴,所以通常選擇去池化,去做共享,但是池化操作本身會引入額外開銷,對於虛擬執行緒池化反而是得不償失,使用虛擬執行緒我們拋棄池化的思維,用時建立,用完就扔。
虛擬執行緒適用場景
大量的 IO 阻塞等待任務,例如下游 RPC 呼叫,DB 查詢等。 大批次的處理時間較短的計算任務。 Thread-per-request (一請求一執行緒)風格的應用程式,例如主流的 Tomcat 執行緒模型或者基於類似執行緒模型實現的 SpringMVC 框架 ,這些應用只需要小小的改動就可以帶來巨大的吞吐提升。
五
虛擬執行緒壓測效能分析
在下面的測試中,我們將模擬最常使用的場景-使用 Web 容器去處理 Http 請求。
場景一:
在 Spring Boot 中使用內嵌的 Tomcat 去處理 Http 請求,使用預設的平臺執行緒池作為 Tomcat 的請求處理執行緒池。場景二:
使用 Spring -WebFlux 建立基於事件迴圈模型的應用程式,進行響應式請求處理。場景三:
在 Spring Boot 中使用內嵌的 Tomcat 去處理 Http 請求,使用虛擬執行緒池作為 Tomcat 的請求處理執行緒池 (Tomcat已支援虛擬執行緒)。測試流程
Jmeter 開啟 500 個執行緒去並行發起請求。每個執行緒將等待請求響應後再發起下一次請求,單次請求超時時間為 10s,測試時間持續 60s。 測試的 Web Server 將接受 Jmeter 的請求,並呼叫慢速伺服器獲取響應並返回。 慢速伺服器以隨機超時響應。最大響應時間為 1000ms。平均響應時間為 500ms。
衡量指標
吞吐量和平均響應時間,吞吐量越高,平均響應時間越低,效能就越好。
Tomcat+普通執行緒池
預設情況下,Tomcat 使用一請求一執行緒模型處理請求,當 Tomcat 收到請求時,會從執行緒池中取一個執行緒去處理請求,該分配的執行緒將一直保持佔用狀態,直到請求結束才會釋放。當執行緒池中沒有執行緒時,請求會一直阻塞在佇列中,直到有請求結束釋放執行緒。預設佇列長度為 Integer.MAX。
預設執行緒池
預設情況下,執行緒池最多包含 200 個執行緒。這基本上意味著單個時間點最多處理 200 個請求。對於每個請求服務都會以阻塞的方式呼叫平均 RT500ms 的慢速伺服器。因此,可以預期每秒 400 個請求的吞吐量,最終壓測結果非常接近預期值,為 388 req/sec。
WebFlux
WebFlux 跟傳統的 Tomcat 執行緒模型不一樣,他不會為每個請求分配一個專用執行緒,而是使用事件迴圈模型透過非阻塞 I/O 操作同時處理多個請求,這使得它能夠用有限的執行緒數量處理大量的併發請求。
@Bean
public WebClient slowServerClient() {
return WebClient.builder()
.baseUrl(")
.build();
}
@Bean
public RouterFunction<ServerResponse> routes(WebClient slowServerClient) {
return route(GET("/"), (ServerRequest req) -> ok()
.body(
slowServerClient
.get()
.exchangeToFlux(resp -> resp.bodyToFlux(Object.class)),
Object.class
));
}
Tomcat+虛擬執行緒池
與平臺執行緒相比,虛擬執行緒的記憶體佔用量要低得多,執行程式大量的建立虛擬執行緒,而不會耗盡系統資源;同時當遇到 Thread.sleep(),CompletableFuture.await(),等待 I/O,獲取鎖時,虛擬執行緒會自動解除安裝,JVM 可以自動切換到另外的等待就緒的虛擬執行緒,提升單個平臺執行緒的利用率,保證平臺執行緒不會浪費在無意義的阻塞等待上。
--enable-preview
,同時 Tomcat 在 10 版本已支援虛擬執行緒,我們只需要替換 Tomcat 的平臺執行緒池為虛擬執行緒池即可。
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandler() {
return protocolHandler ->
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
}
private final RestTemplate restTemplate;
@GetMapping
public ResponseEntity<Object> callSlowServer(){
return restTemplate.getForEntity(", Object.class);
}
傳統的執行緒池模式效果差強人意,可以透過提高執行緒數量可以提升吞吐,但是需要考慮到系統容量和資源限制,但是對於大部分場景來說使用執行緒池去處理阻塞操作仍然是主流且不錯的選擇。 WebFlux 的效果非常好,但是考慮到需要完全按照響應式風格進行開發,成本及難度較大,同時 WebFlux 與現有的一些主流框架存在一些相容問題,例如 Mysql 官方 IO 庫不支援 NIO、Threadlocal 相容問題等等。現有應用的遷移基本要重寫所有程式碼,改動量和風險都不可控。 虛擬執行緒的效果非常好, 最大的優勢就是我們沒有修改程式碼或採用任何反應式技術,唯一更改是將執行緒池替換為虛擬執行緒
。雖然改動較小,但與使用執行緒池相比,效能結果得到了顯著改善。
六
總結
過去很長時間,在編寫服務端應用時,我們對於每個請求,都使用獨佔的執行緒來處理,請求之間是相互獨立的,這就是 一請求一執行緒的模型
這種方式易於理解和程式設計實現,也易於除錯和效能調優。
然而,一請求一執行緒風格並不能簡單地使用平臺執行緒來實現,因為平臺執行緒是作業系統中執行緒的封裝。作業系統的執行緒會申請成本較高,存在數量上限。對於一個要併發處理海量請求的伺服器端應用來說,對每個請求都建立一個平臺執行緒是不現實的。在這種前提下,湧現出一批非阻塞 I/O 和非同步程式設計框架,如 WebFlux ,RX-Java。當某個請求在等待 I/O 操作時,它會暫時讓出執行緒,並在 I/O 操作完成之後繼續執行。透過這種方式,可以用少量執行緒同時處理大量的請求。這些框架可以提升系統的吞吐量,但是要求開發人員必須熟悉所使用的底層框架,並按照響應式的風格來編寫程式碼,響應式框架的除錯困難,學習成本,相容問題使得大部分人望而卻步 。
在使用虛擬執行緒之後,一切都將改變,開發人員可以使用目前最習慣舒服的方式來編寫程式碼,高效能和高吞吐由虛擬執行緒自動幫你完成,這極大地降低了編寫高併發服務應用的難度。
參考文件
%E5%89%8D%E6%8F%90
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70027824/viewspace-2993461/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 虛擬執行緒原理及效能分析執行緒
- Java執行緒池原理及分析Java執行緒
- 深入理解Sora技術原理|得物技術Sora
- [Java基礎]虛擬執行緒Java執行緒
- 執行緒剖析 - 助力定位程式碼層面高耗時問題|得物技術執行緒
- 硬核虛擬化技術 SR-IOV的原理及探索
- 多執行緒下的網格生成及效能分析執行緒
- JUC(4)---java執行緒池原理及原始碼分析Java執行緒原始碼
- 多執行緒:原理分析整理執行緒
- 使用Loom建立虛擬執行緒 - davidOOM執行緒
- 深入理解多執行緒(五)—— Java虛擬機器的鎖優化技術執行緒Java虛擬機優化
- 【得物技術】深入理解synchronzied底層原理
- java 21 虛擬執行緒初體驗Java執行緒
- 3 分鐘理解 Java 虛擬執行緒Java執行緒
- javascript執行緒及與執行緒有關的效能優化JavaScript執行緒優化
- Java 21 的虛擬執行緒:高效能併發應用的福音Java執行緒
- 多執行緒核心技術(1)-執行緒的基本方法執行緒
- JAVA執行緒池的原理及使用Java執行緒
- Java執行緒池原始碼及原理Java執行緒原始碼
- 得物直播低延遲探索 | 得物技術
- Java中CompletableFuture與虛擬執行緒比較Java執行緒
- Java 21 新特性:虛擬執行緒(Virtual Threads)Java執行緒thread
- Java 21 虛擬執行緒:使用指南(一)Java執行緒
- 聊聊JDK19特性之虛擬執行緒JDK執行緒
- [深入理解Java虛擬機器]執行緒Java虛擬機執行緒
- Java“虛擬執行緒”被提交到JEP草案Java執行緒
- 支援JDK19虛擬執行緒的web框架,之四:看原始碼,瞭解quarkus如何支援虛擬執行緒JDK執行緒Web框架原始碼
- 【併發技術01】傳統執行緒技術中建立執行緒的兩種方式執行緒
- 彩虹橋架構演進之路-效能篇|得物技術架構
- 多執行緒引起的效能問題分析執行緒
- Java 21 正式 GA,虛擬執行緒真的來了Java執行緒
- Redis執行緒模型的原理分析蒼癘Redis執行緒模型
- Java併發/多執行緒-CAS原理分析Java執行緒
- 虛擬機器遷移技術原理與應用虛擬機
- java核心技術筆記--執行緒Java筆記執行緒
- 保證執行緒安全的技術執行緒
- 虛擬化四、KVM虛擬化技術
- LUA指令碼虛擬機器逃逸技術分析指令碼虛擬機