本" />

AVt天堂网 手机版,亚洲va久久久噜噜噜久久4399,天天综合亚洲色在线精品,亚洲一级Av无码毛片久久精品

當(dāng)前位置:首頁(yè) > 科技  > 軟件

虛擬線程原理及性能分析

來(lái)源: 責(zé)編: 時(shí)間:2023-11-07 09:14:46 328觀看
導(dǎo)讀一、背景JDK21 在 9 月 19 號(hào)正式發(fā)布,帶來(lái)了較多亮點(diǎn),其中虛擬線程備受矚目,毫不夸張的說(shuō),它改變了高吞吐代碼的編寫(xiě)方式,只需要小小的變動(dòng)就可以讓目前的 IO 密集型程序的吞吐量得到提升,寫(xiě)出高吞吐量的代碼不再困難。

一、背景

JDK21 在 9 月 19 號(hào)正式發(fā)布,帶來(lái)了較多亮點(diǎn),其中虛擬線程備受矚目,毫不夸張的說(shuō),它改變了高吞吐代碼的編寫(xiě)方式,只需要小小的變動(dòng)就可以讓目前的 IO 密集型程序的吞吐量得到提升,寫(xiě)出高吞吐量的代碼不再困難。
WA328資訊網(wǎng)——每日最新資訊28at.com

本文將詳細(xì)介紹虛擬線程的使用場(chǎng)景,實(shí)現(xiàn)原理以及在 IO 密集型服務(wù)下的性能壓測(cè)效果。WA328資訊網(wǎng)——每日最新資訊28at.com

二、為了提升吞吐性能,我們所做的優(yōu)化

在講虛擬線程之前,我們先聊聊為了提高吞吐性能,我們所做的一些優(yōu)化方案。
WA328資訊網(wǎng)——每日最新資訊28at.com

串行模式

在當(dāng)前的微服務(wù)架構(gòu)下,處理一次用戶(hù)/上游的請(qǐng)求,往往需要多次調(diào)用下游服務(wù)、數(shù)據(jù)庫(kù)、文件系統(tǒng)等,再將所有請(qǐng)求的數(shù)據(jù)進(jìn)行處理最終的結(jié)果返回給上游。
WA328資訊網(wǎng)——每日最新資訊28at.com

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

在這種模式下,使用串行模式去查詢(xún)數(shù)據(jù)庫(kù),下游 Dubbo/Http 接口,文件系統(tǒng)完成一次請(qǐng)求,接口整體的耗時(shí)等于各個(gè)下游的返回時(shí)間之和,這種寫(xiě)法雖然簡(jiǎn)單,但是接口耗時(shí)長(zhǎng)、性能差,無(wú)法滿足 C 端高 QPS 場(chǎng)景下的性能要求。WA328資訊網(wǎng)——每日最新資訊28at.com

線程池+Future異步調(diào)用

為了解決串行調(diào)用的低性能問(wèn)題,我們會(huì)考慮使用并行異步調(diào)用的方式,最簡(jiǎn)單的方式便是使用線程池 +Future 去并行調(diào)用。
WA328資訊網(wǎng)——每日最新資訊28at.com

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

典型代碼如下:WA328資訊網(wǎng)——每日最新資訊28at.com

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

這種方式雖然解決了大部分場(chǎng)景下的串行調(diào)用低性能問(wèn)題,但是也存在著嚴(yán)重的弊端,由于存在 Future 的前后依賴(lài)關(guān)系,當(dāng)使用場(chǎng)景存在大量的前后依賴(lài)時(shí),會(huì)使得線程資源和 CPU 大量浪費(fèi)在阻塞等待上,導(dǎo)致資源利用率低。WA328資訊網(wǎng)——每日最新資訊28at.com

線程池+CompletableFuture異步調(diào)用

為了降低 CPU 的阻塞等待時(shí)間和提升資源的利用率,我們會(huì)使用CompletableFuture對(duì)調(diào)用流程進(jìn)行編排,降低依賴(lài)之間的阻塞。
WA328資訊網(wǎng)——每日最新資訊28at.com

CompletableFuture 是由 Java8 引入的,在 Java8 之前一般通過(guò) Future 實(shí)現(xiàn)異步。Future 用于表示異步計(jì)算的結(jié)果,如果存在流程之間的依賴(lài)關(guān)系,那么只能通過(guò)阻塞或者輪詢(xún)的方式獲取結(jié)果,同時(shí)原生的 Future 不支持設(shè)置回調(diào)方法,Java8 之前若要設(shè)置回調(diào)可以使用 Guava 的 ListenableFuture,回調(diào)的引入又會(huì)導(dǎo)致回調(diào)地獄,代碼基本不具備可讀性。WA328資訊網(wǎng)——每日最新資訊28at.com

而 CompletableFuture 是對(duì) Future 的擴(kuò)展,原生支持通過(guò)設(shè)置回調(diào)的方式處理計(jì)算結(jié)果,同時(shí)也支持組合編排操作,一定程度解決了回調(diào)地獄的問(wèn)題。WA328資訊網(wǎng)——每日最新資訊28at.com

使用 CompletableFuture 的實(shí)現(xiàn)方式如下:WA328資訊網(wǎng)——每日最新資訊28at.com

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

CompletableFuture 雖然一定程度上面緩解了 CPU 資源大量浪費(fèi)在阻塞等待上的問(wèn)題,但是只是緩解,核心的問(wèn)題始終沒(méi)有解決。這兩個(gè)問(wèn)題導(dǎo)致 CPU 無(wú)法充分被利用,系統(tǒng)吞吐量容易達(dá)到瓶頸。WA328資訊網(wǎng)——每日最新資訊28at.com

線程資源浪費(fèi)瓶頸始終在 IO 等待上,導(dǎo)致 CPU 資源利用率較低。目前大部分服務(wù)是 IO 密集型服務(wù),一次請(qǐng)求的處理耗時(shí)大部分都消耗在等待下游 RPC,數(shù)據(jù)庫(kù)查詢(xún)的 IO 等待中,此時(shí)線程仍然只能阻塞等待結(jié)果返回,導(dǎo)致 CPU 的利用率很低。WA328資訊網(wǎng)——每日最新資訊28at.com

線程數(shù)量存在限制, 為了增加并發(fā)度,我們會(huì)給線程池配置更大的線程數(shù),但是線程的數(shù)量是有限制的,Java 的線程模型是 1:1 映射平臺(tái)線程的,導(dǎo)致 Java 線程創(chuàng)建的成本很高,不能無(wú)限增加。同時(shí)隨著 CPU 調(diào)度線程數(shù)的增加,會(huì)導(dǎo)致更嚴(yán)重的資源爭(zhēng)用,寶貴的 CPU 資源被損耗在上下文切換上。WA328資訊網(wǎng)——每日最新資訊28at.com

三、一請(qǐng)求一線程的模型

在給出最終解決方案之前,我們先聊一聊 Web 應(yīng)用中常見(jiàn)的一請(qǐng)求一線程的模型。
WA328資訊網(wǎng)——每日最新資訊28at.com

在 Web 中我們最常見(jiàn)的請(qǐng)求模型就是使用一請(qǐng)求一線程的模型,每個(gè)請(qǐng)求都由單獨(dú)的線程處理。此模型易于理解和實(shí)現(xiàn),對(duì)編碼的可讀性,Debug 都非常友好,但是,它有一些缺點(diǎn)。當(dāng)線程執(zhí)行阻塞操作(如連接到數(shù)據(jù)庫(kù)或進(jìn)行網(wǎng)絡(luò)調(diào)用)時(shí),線程會(huì)被阻塞,直到操作完成,這意味著線程在此期間將無(wú)法處理任何其他請(qǐng)求。WA328資訊網(wǎng)——每日最新資訊28at.com

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

當(dāng)遇到大促或突發(fā)流量等場(chǎng)景導(dǎo)致服務(wù)承受的請(qǐng)求數(shù)增大時(shí),為了保證每個(gè)請(qǐng)求在盡可能短的時(shí)間內(nèi)返回,減少等待時(shí)間,我們經(jīng)常會(huì)采用以下方案:WA328資訊網(wǎng)——每日最新資訊28at.com

  • 擴(kuò)大服務(wù)最大線程數(shù),簡(jiǎn)單有效,由于存在下列問(wèn)題,導(dǎo)致平臺(tái)線程有最大數(shù)量限制,不能大量擴(kuò)充。

系統(tǒng)資源有限導(dǎo)致系統(tǒng)線程總量有限,進(jìn)而導(dǎo)致與系統(tǒng)線程一一對(duì)應(yīng)的平臺(tái)線程有限。WA328資訊網(wǎng)——每日最新資訊28at.com

平臺(tái)線程的調(diào)度依賴(lài)于系統(tǒng)的線程調(diào)度程序,當(dāng)平臺(tái)線程創(chuàng)建過(guò)多,會(huì)消耗大量資源用于處理線程上下文切換。WA328資訊網(wǎng)——每日最新資訊28at.com

每個(gè)平臺(tái)線程都會(huì)開(kāi)辟一塊大小約 1m 私有的棧空間,大量平臺(tái)線程會(huì)占據(jù)大量?jī)?nèi)存。WA328資訊網(wǎng)——每日最新資訊28at.com

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

  • 垂直擴(kuò)展,升級(jí)機(jī)器配置,水平擴(kuò)展,增加服務(wù)節(jié)點(diǎn),也就是俗稱(chēng)的升配擴(kuò)容大法,效果好,也是最常見(jiàn)的方案,缺點(diǎn)是會(huì)增加成本,同時(shí)有些場(chǎng)景下擴(kuò)容并不能 100% 解決問(wèn)題。
  • 采用異步/響應(yīng)式編程方案,例如 RPC NIO 異步調(diào)用,WebFlux,Rx-Java 等非阻塞的基于 Ractor 模型的框架,使用事件驅(qū)動(dòng)使得少量線程即可實(shí)現(xiàn)高吞吐的請(qǐng)求處理,擁有較好的性能與優(yōu)秀的資源利用,缺點(diǎn)是學(xué)習(xí)成本較高兼容性問(wèn)題較大,編碼風(fēng)格與目前的一請(qǐng)求一線程的模型差異較大,理解難度大,同時(shí)對(duì)于代碼的調(diào)試比較困難。

那么有沒(méi)有一種方法可以易于編寫(xiě),方便遷移,符合日常編碼習(xí)慣,同時(shí)性能很不錯(cuò),CPU 資源利用率較高的方案呢?WA328資訊網(wǎng)——每日最新資訊28at.com

JDK21 中的虛擬線程可能給出了答案, JDK 提供了與 Thread 完全一致的抽象 Virtual Thread 來(lái)應(yīng)對(duì)這種經(jīng)常阻塞的情況,阻塞仍然是會(huì)阻塞,但是換了阻塞的對(duì)象,由昂貴的平臺(tái)線程阻塞改為了成本很低的虛擬線程的阻塞,當(dāng)代碼調(diào)用到阻塞 API 例如 IO,同步,Sleep 等操作時(shí),JVM 會(huì)自動(dòng)把 Virtual Thread 從平臺(tái)線程上卸載,平臺(tái)線程就會(huì)去處理下一個(gè)虛擬線程,通過(guò)這種方式,提升了平臺(tái)線程的利用率,讓平臺(tái)線程不再阻塞在等待上,從底層實(shí)現(xiàn)了少量平臺(tái)線程就可以處理大量請(qǐng)求,提高了服務(wù)吞吐和 CPU 的利用率。WA328資訊網(wǎng)——每日最新資訊28at.com

四、虛擬線程

線程術(shù)語(yǔ)定義

操作系統(tǒng)線程(OS Thread):由操作系統(tǒng)管理,是操作系統(tǒng)調(diào)度的基本單位。
WA328資訊網(wǎng)——每日最新資訊28at.com

平臺(tái)線程(Platform Thread):Java.Lang.Thread 類(lèi)的每個(gè)實(shí)例,都是一個(gè)平臺(tái)線程,是 Java 對(duì)操作系統(tǒng)線程的包裝,與操作系統(tǒng)是 1:1 映射。WA328資訊網(wǎng)——每日最新資訊28at.com

虛擬線程(Virtual Thread):一種輕量級(jí),由 JVM 管理的線程。對(duì)應(yīng)的實(shí)例 java.lang.VirtualThread 這個(gè)類(lèi)。WA328資訊網(wǎng)——每日最新資訊28at.com

載體線程(Carrier Thread):指真正負(fù)責(zé)執(zhí)行虛擬線程中任務(wù)的平臺(tái)線程。一個(gè)虛擬線程裝載到一個(gè)平臺(tái)線程之后,那么這個(gè)平臺(tái)線程就被稱(chēng)為虛擬線程的載體線程。WA328資訊網(wǎng)——每日最新資訊28at.com

虛擬線程定義

JDK 中 java.lang.Thread 的每個(gè)實(shí)例都是一個(gè)平臺(tái)線程。平臺(tái)線程在底層操作系統(tǒng)線程上運(yùn)行 Java 代碼,并在代碼的整個(gè)生命周期內(nèi)獨(dú)占操作系統(tǒng)線程,平臺(tái)線程實(shí)例本質(zhì)是由系統(tǒng)內(nèi)核的線程調(diào)度程序進(jìn)行調(diào)度,并且平臺(tái)線程的數(shù)量受限于操作系統(tǒng)線程的數(shù)量。
WA328資訊網(wǎng)——每日最新資訊28at.com

而虛擬線程(Virtual Thread)它不與特定的操作系統(tǒng)線程相綁定。它在平臺(tái)線程上運(yùn)行 Java 代碼,但在代碼的整個(gè)生命周期內(nèi)不獨(dú)占平臺(tái)線程。這意味著許多虛擬線程可以在同一個(gè)平臺(tái)線程上運(yùn)行他們的 Java 代碼,共享同一個(gè)平臺(tái)線程。同時(shí)虛擬線程的成本很低,虛擬線程的數(shù)量可以比平臺(tái)線程的數(shù)量大得多。WA328資訊網(wǎng)——每日最新資訊28at.com

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

虛擬線程創(chuàng)建

方法一:直接創(chuàng)建虛擬線程WA328資訊網(wǎng)——每日最新資訊28at.com

Thread vt = Thread.startVirtualThread(() -> {    System.out.println("hello wolrd virtual thread");});

方法二:創(chuàng)建虛擬線程但不自動(dòng)運(yùn)行,手動(dòng)調(diào)用start()開(kāi)始運(yùn)行WA328資訊網(wǎng)——每日最新資訊28at.com

Thread.ofVirtual().unstarted(() -> {    System.out.println("hello wolrd virtual thread");});vt.start();

方法三:通過(guò)虛擬線程的 ThreadFactory 創(chuàng)建虛擬線程WA328資訊網(wǎng)——每日最新資訊28at.com

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();ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();executor.submit(() -> {    System.out.println("Start virtual thread...");    Thread.sleep(1000);    System.out.println("End virtual thread.");    return true;});

虛擬線程實(shí)現(xiàn)原理

虛擬線程是由 Java 虛擬機(jī)調(diào)度,而不是操作系統(tǒng)。虛擬線程占用空間小,同時(shí)使用輕量級(jí)的任務(wù)隊(duì)列來(lái)調(diào)度虛擬線程,避免了線程間基于內(nèi)核的上下文切換開(kāi)銷(xiāo),因此可以極大量地創(chuàng)建和使用。
WA328資訊網(wǎng)——每日最新資訊28at.com

簡(jiǎn)單來(lái)看,虛擬線程實(shí)現(xiàn)如下:virtual thread =continuation+scheduler+runnableWA328資訊網(wǎng)——每日最新資訊28at.com

虛擬線程會(huì)把任務(wù)(java.lang.Runnable實(shí)例)包裝到一個(gè) Continuation 實(shí)例中:WA328資訊網(wǎng)——每日最新資訊28at.com

  • 當(dāng)任務(wù)需要阻塞掛起的時(shí)候,會(huì)調(diào)用 Continuation 的 yield 操作進(jìn)行阻塞,虛擬線程會(huì)從平臺(tái)線程卸載。
  • 當(dāng)任務(wù)解除阻塞繼續(xù)執(zhí)行的時(shí)候,調(diào)用 Continuation.run 會(huì)從阻塞點(diǎn)繼續(xù)執(zhí)行。

Scheduler 也就是執(zhí)行器,由它將任務(wù)提交到具體的載體線程池中執(zhí)行。WA328資訊網(wǎng)——每日最新資訊28at.com

  • 它是 java.util.concurrent.Executor 的子類(lèi)。
  • 虛擬線程框架提供了一個(gè)默認(rèn)的 FIFO 的 ForkJoinPool 用于執(zhí)行虛擬線程任務(wù)。

Runnable 則是真正的任務(wù)包裝器,由 Scheduler 負(fù)責(zé)提交到載體線程池中執(zhí)行。WA328資訊網(wǎng)——每日最新資訊28at.com

JVM 把虛擬線程分配給平臺(tái)線程的操作稱(chēng)為 mount(掛載),取消分配平臺(tái)線程的操作稱(chēng)為 unmount(卸載):WA328資訊網(wǎng)——每日最新資訊28at.com

mount 操作:虛擬線程掛載到平臺(tái)線程,虛擬線程中包裝的 Continuation 堆棧幀數(shù)據(jù)會(huì)被拷貝到平臺(tái)線程的線程棧,這是一個(gè)從堆復(fù)制到棧的過(guò)程。WA328資訊網(wǎng)——每日最新資訊28at.com

unmount 操作:虛擬線程從平臺(tái)線程卸載,此時(shí)虛擬線程的任務(wù)還沒(méi)有執(zhí)行完成,所以虛擬線程中包裝的 Continuation 棧數(shù)據(jù)幀會(huì)會(huì)留在堆內(nèi)存中。WA328資訊網(wǎng)——每日最新資訊28at.com

從 Java 代碼的角度來(lái)看,其實(shí)是看不到虛擬線程及載體線程共享操作系統(tǒng)線程的,會(huì)認(rèn)為虛擬線程及其載體都在同一個(gè)線程上運(yùn)行,因此,在同一虛擬線程上多次調(diào)用的代碼可能會(huì)在每次調(diào)用時(shí)掛載的載體線程都不一樣。JDK 中使用了 FIFO 模式的 ForkJoinPool 作為虛擬線程的調(diào)度器,從這個(gè)調(diào)度器看虛擬線程任務(wù)的執(zhí)行流程大致如下:WA328資訊網(wǎng)——每日最新資訊28at.com

  • 調(diào)度器(線程池)中的平臺(tái)線程等待處理任務(wù)。

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

  • 一個(gè)虛擬線程被分配平臺(tái)線程,該平臺(tái)線程作為載體線程執(zhí)行虛擬線程中的任務(wù)。

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

  • 虛擬線程運(yùn)行其 Continuation,Mount(掛載)平臺(tái)線程后,最終執(zhí)行 Runnable 包裝的用戶(hù)實(shí)際任務(wù)。

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

  • 虛擬線程任務(wù)執(zhí)行完成,標(biāo)記 Continuation 終結(jié),標(biāo)記虛擬線程為終結(jié)狀態(tài),清空上下文,等待 GC 回收,解除掛載載體線程會(huì)返還到調(diào)度器(線程池)中等待處理下一個(gè)任務(wù)。

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

上面是沒(méi)有阻塞場(chǎng)景的虛擬線程任務(wù)執(zhí)行情況,如果遇到了阻塞(例如 Lock 等)場(chǎng)景,會(huì)觸發(fā) Continuation 的 yield 操作讓出控制權(quán),等待虛擬線程重新分配載體線程并且執(zhí)行,具體見(jiàn)下面的代碼:WA328資訊網(wǎng)——每日最新資訊28at.com

ReentrantLock lock = new ReentrantLock();        Thread.startVirtualThread(() -> {            lock.lock();            });        // 確保鎖已經(jīng)被上面的虛擬線程持有        Thread.sleep(1000);          Thread.startVirtualThread(() -> {            System.out.println("first");            會(huì)觸發(fā)Continuation的yield操作            lock.lock();             try {                System.out.println("second");            } finally {                lock.unlock();            }            System.out.println("third");        });        Thread.sleep(Long.MAX_VALUE);    }
  • 虛擬線程中任務(wù)執(zhí)行時(shí)候調(diào)用 Continuation#run() 先執(zhí)行了部分任務(wù)代碼,然后嘗試獲取鎖,該操作是阻塞操作會(huì)導(dǎo)致 Continuation 的 yield 操作讓出控制權(quán),如果 yield 操作成功,會(huì)從載體線程 unmount,載體線程棧數(shù)據(jù)會(huì)移動(dòng)到 Continuation 棧的數(shù)據(jù)幀中,保存在堆內(nèi)存中,虛擬線程任務(wù)完成,此時(shí)虛擬線程和 Continuation 還沒(méi)有終結(jié)和釋放,載體線程被釋放到執(zhí)行器中等待新的任務(wù);如果 Continuation 的 yield 操作失敗,則會(huì)對(duì)載體線程進(jìn)行 Park 調(diào)用,阻塞在載體線程上,此時(shí)虛擬線程和載體線程同時(shí)會(huì)被阻塞,本地方法,Synchronized 修飾的同步方法都會(huì)導(dǎo)致 yield 失敗。

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

  • 當(dāng)鎖持有者釋放鎖之后,會(huì)喚醒虛擬線程獲取鎖,獲取鎖成功后,虛擬線程會(huì)重新進(jìn)行 mount,讓虛擬線程任務(wù)再次執(zhí)行,此時(shí)有可能是分配到另一個(gè)載體線程中執(zhí)行,Continuation 棧會(huì)的數(shù)據(jù)幀會(huì)被恢復(fù)到載體線程棧中,然后再次調(diào)用Continuation#run() 恢復(fù)任務(wù)執(zhí)行。

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

  • 虛擬線程任務(wù)執(zhí)行完成,標(biāo)記 Continuation 終結(jié),標(biāo)記虛擬線程為終結(jié)狀態(tài),清空上下文變量,解除載體線程的掛載載體線程返還到調(diào)度器(線程池)中作為平臺(tái)線程等待處理下一個(gè)任務(wù)。

Continuation 組件十分重要,它既是用戶(hù)真實(shí)任務(wù)的包裝器,同時(shí)提供了虛擬線程任務(wù)暫停/繼續(xù)的能力,以及虛擬線程與平臺(tái)線程數(shù)據(jù)轉(zhuǎn)移功能,當(dāng)任務(wù)需要阻塞掛起的時(shí)候,調(diào)用 Continuation 的 yield 操作進(jìn)行阻塞。當(dāng)任務(wù)需要解除阻塞繼續(xù)執(zhí)行的時(shí)候,則調(diào)用 Continuation 的 run 恢復(fù)執(zhí)行。WA328資訊網(wǎng)——每日最新資訊28at.com

通過(guò)下面的代碼可以看出 Continuation 的神奇之處,通過(guò)在編譯參數(shù)加上--add-exports java.base/jdk.internal.vm=ALL-UNNAMED 可以在本地運(yùn)行。WA328資訊網(wǎng)——每日最新資訊28at.com

ContinuationScope scope = new ContinuationScope("scope");Continuation continuation = new Continuation(scope, () -> {    System.out.println("before yield開(kāi)始");    Continuation.yield(scope);    System.out.println("after yield 結(jié)束");});System.out.println("1 run");// 第一次執(zhí)行Continuation.runcontinuation.run();System.out.println("2 run");// 第二次執(zhí)行Continuation.runcontinuation.run();System.out.println("Done");

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

通過(guò)上述案例可以看出,Continuation 實(shí)例進(jìn)行 yield 調(diào)用后,再次調(diào)用其 run 方法就可以從 yield 的調(diào)用之處繼續(xù)往下執(zhí)行,從而實(shí)現(xiàn)了程序的中斷和恢復(fù)。WA328資訊網(wǎng)——每日最新資訊28at.com

虛擬線程內(nèi)存占用評(píng)估WA328資訊網(wǎng)——每日最新資訊28at.com

單個(gè)平臺(tái)線程的資源占用:

  • 根據(jù) JVM 規(guī)范,預(yù)留 1 MB 線程棧空間。
  • 平臺(tái)線程實(shí)例,會(huì)占據(jù) 2000+ byte 數(shù)據(jù)。

單個(gè)虛擬線程的資源占用:WA328資訊網(wǎng)——每日最新資訊28at.com

  • Continuation 棧會(huì)占用數(shù)百 byte 到數(shù)百 KB 內(nèi)存空間,是作為堆棧塊對(duì)象存儲(chǔ)在 Java 堆中。
  • 虛擬線程實(shí)例會(huì)占據(jù) 200 - 240 byte 數(shù)據(jù)。

從對(duì)比結(jié)果來(lái)看,理論上單個(gè)平臺(tái)線程占用的內(nèi)存空間至少是 KB 級(jí)別的,而單個(gè)虛擬線程實(shí)例占用的內(nèi)存空間是 byte 級(jí)別,兩者的內(nèi)存占用差距較大,這也是虛擬線程可以大批量創(chuàng)建的原因。WA328資訊網(wǎng)——每日最新資訊28at.com

下面通過(guò)一段程序去測(cè)試平臺(tái)線程和虛擬線程的內(nèi)存占用:WA328資訊網(wǎng)——每日最新資訊28at.com

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);}

上面的程序運(yùn)行后啟動(dòng) 4000 平臺(tái)線程,通過(guò) -XX:NativeMemoryTracking=detail 參數(shù)和 JCMD 命令查看所有線程占據(jù)的內(nèi)存空間如下:WA328資訊網(wǎng)——每日最新資訊28at.com

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

內(nèi)存占用大部分來(lái)自創(chuàng)建的平臺(tái)線程,總線程棧空間占用約為 8096 MB,兩者加起來(lái)占據(jù)總使用內(nèi)存(8403MB)的 96% 以上。WA328資訊網(wǎng)——每日最新資訊28at.com

用類(lèi)似的方式編寫(xiě)運(yùn)行虛擬線程的程序:WA328資訊網(wǎng)——每日最新資訊28at.com

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);}

上面的程序運(yùn)行后啟動(dòng) 4000 虛擬線程:WA328資訊網(wǎng)——每日最新資訊28at.com

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

堆內(nèi)存的實(shí)際占用量和總內(nèi)存的實(shí)際占用量都不超過(guò) 300 MB,可以證明虛擬線程在大量創(chuàng)建的前提下也不會(huì)去占用過(guò)多的內(nèi)存,且虛擬線程的堆棧是作為堆棧塊對(duì)象存儲(chǔ)在 Java 的堆中的,可以被 GC 回收,又降低了虛擬線程的占用。WA328資訊網(wǎng)——每日最新資訊28at.com

虛擬線程的局限及使用建議WA328資訊網(wǎng)——每日最新資訊28at.com

  • 虛擬線程存在 native 方法或者外部方法 (Foreign Function & Memory API,jep 424 ) 調(diào)用不能進(jìn)行 yield 操作,此時(shí)載體線程會(huì)被阻塞。
  • 當(dāng)運(yùn)行在 synchronized 修飾的代碼塊或者方法時(shí),不能進(jìn)行 yield 操作,此時(shí)載體線程會(huì)被阻塞,推薦使用 ReentrantLock。
  • ThreadLocal 相關(guān)問(wèn)題,目前虛擬線程仍然是支持 ThreadLocal 的,但是由于虛擬線程的數(shù)量非常多,會(huì)導(dǎo)致 Threadlocal 中存的線程變量非常多,需要頻繁 GC 去清理,對(duì)性能會(huì)有影響,官方建議盡量少使用 ThreadLocal,同時(shí)不要在虛擬線程的 ThreadLocal 中放大對(duì)象,目前官方是想通過(guò) ScopedLocal 去替換掉 ThreadLocal,但是在 21 版本還沒(méi)有正式發(fā)布,這個(gè)可能是大規(guī)模使用虛擬線程的一大難題。
  • 無(wú)需池化虛擬線程 虛擬線程占用的資源很少,因此可以大量地創(chuàng)建而無(wú)須考慮池化,它不需要跟平臺(tái)線程池一樣,平臺(tái)線程的創(chuàng)建成本比較昂貴,所以通常選擇去池化,去做共享,但是池化操作本身會(huì)引入額外開(kāi)銷(xiāo),對(duì)于虛擬線程池化反而是得不償失,使用虛擬線程我們拋棄池化的思維,用時(shí)創(chuàng)建,用完就扔。

虛擬線程適用場(chǎng)景

  • 大量的 IO 阻塞等待任務(wù),例如下游 RPC 調(diào)用,DB 查詢(xún)等。
  • 大批量的處理時(shí)間較短的計(jì)算任務(wù)。
  • Thread-per-request (一請(qǐng)求一線程)風(fēng)格的應(yīng)用程序,例如主流的 Tomcat 線程模型或者基于類(lèi)似線程模型實(shí)現(xiàn)的 SpringMVC 框架 ,這些應(yīng)用只需要小小的改動(dòng)就可以帶來(lái)巨大的吞吐提升。

五、虛擬線程壓測(cè)性能分析

在下面的測(cè)試中,我們將模擬最常使用的場(chǎng)景-使用 Web 容器去處理 Http 請(qǐng)求。
WA328資訊網(wǎng)——每日最新資訊28at.com

場(chǎng)景一:在 Spring Boot 中使用內(nèi)嵌的 Tomcat 去處理 Http 請(qǐng)求,使用默認(rèn)的平臺(tái)線程池作為 Tomcat 的請(qǐng)求處理線程池。WA328資訊網(wǎng)——每日最新資訊28at.com

場(chǎng)景二:使用 Spring -WebFlux 創(chuàng)建基于事件循環(huán)模型的應(yīng)用程序,進(jìn)行響應(yīng)式請(qǐng)求處理。WA328資訊網(wǎng)——每日最新資訊28at.com

場(chǎng)景三:在 Spring Boot 中使用內(nèi)嵌的 Tomcat 去處理 Http 請(qǐng)求,使用虛擬線程池作為 Tomcat 的請(qǐng)求處理線程池 (Tomcat已支持虛擬線程)。WA328資訊網(wǎng)——每日最新資訊28at.com

測(cè)試流程

  • Jmeter 開(kāi)啟 500 個(gè)線程去并行發(fā)起請(qǐng)求。每個(gè)線程將等待請(qǐng)求響應(yīng)后再發(fā)起下一次請(qǐng)求,單次請(qǐng)求超時(shí)時(shí)間為 10s,測(cè)試時(shí)間持續(xù) 60s。
  • 測(cè)試的 Web Server 將接受 Jmeter 的請(qǐng)求,并調(diào)用慢速服務(wù)器獲取響應(yīng)并返回。
  • 慢速服務(wù)器以隨機(jī)超時(shí)響應(yīng)。最大響應(yīng)時(shí)間為 1000ms。平均響應(yīng)時(shí)間為 500ms。

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

衡量指標(biāo)

吞吐量和平均響應(yīng)時(shí)間,吞吐量越高,平均響應(yīng)時(shí)間越低,性能就越好。

Tomcat+普通線程池

默認(rèn)情況下,Tomcat 使用一請(qǐng)求一線程模型處理請(qǐng)求,當(dāng) Tomcat 收到請(qǐng)求時(shí),會(huì)從線程池中取一個(gè)線程去處理請(qǐng)求,該分配的線程將一直保持占用狀態(tài),直到請(qǐng)求結(jié)束才會(huì)釋放。當(dāng)線程池中沒(méi)有線程時(shí),請(qǐng)求會(huì)一直阻塞在隊(duì)列中,直到有請(qǐng)求結(jié)束釋放線程。默認(rèn)隊(duì)列長(zhǎng)度為 Integer.MAX。默認(rèn)線程池默認(rèn)情況下,線程池最多包含 200 個(gè)線程。這基本上意味著單個(gè)時(shí)間點(diǎn)最多處理 200 個(gè)請(qǐng)求。對(duì)于每個(gè)請(qǐng)求服務(wù)都會(huì)以阻塞的方式調(diào)用平均 RT500ms 的慢速服務(wù)器。因此,可以預(yù)期每秒 400 個(gè)請(qǐng)求的吞吐量,最終壓測(cè)結(jié)果非常接近預(yù)期值,為 388 req/sec。
WA328資訊網(wǎng)——每日最新資訊28at.com

圖片WA328資訊網(wǎng)——每日最新資訊28at.com

增加線程池WA328資訊網(wǎng)——每日最新資訊28at.com

生產(chǎn)環(huán)境為了吞吐考慮,一般不會(huì)使用默認(rèn)值,會(huì)把線程池增大到 server.tomcat.threads.max=500+,調(diào)整到 500+ 之后的壓測(cè)結(jié)果如下:WA328資訊網(wǎng)——每日最新資訊28at.com

圖片WA328資訊網(wǎng)——每日最新資訊28at.com

可以看出最終的吞吐量和線程數(shù)量呈比例上升,同時(shí)由于線程數(shù)的增加,請(qǐng)求等待減少,平均 RT 趨向于慢速服務(wù)器的響應(yīng)平均 RT。WA328資訊網(wǎng)——每日最新資訊28at.com

但是需要注意的是,平臺(tái)線程的創(chuàng)建受到內(nèi)存和 Java 線程映射模型的限制,不能無(wú)限擴(kuò)展,同時(shí)大量線程會(huì)導(dǎo)致 CPU 資源大量消耗在上下文切換時(shí),整體性能反而降低。WA328資訊網(wǎng)——每日最新資訊28at.com

WebFlux

WebFlux 跟傳統(tǒng)的 Tomcat 線程模型不一樣,他不會(huì)為每個(gè)請(qǐng)求分配一個(gè)專(zhuān)用線程,而是使用事件循環(huán)模型通過(guò)非阻塞 I/O 操作同時(shí)處理多個(gè)請(qǐng)求,這使得它能夠用有限的線程數(shù)量處理大量的并發(fā)請(qǐng)求。
WA328資訊網(wǎng)——每日最新資訊28at.com

在壓測(cè)的場(chǎng)景下,使用 WebClient 來(lái)進(jìn)行一個(gè)非阻塞的 Http 調(diào)用慢速處理器,并使用 RouterFunction 來(lái)做請(qǐng)求映射和處理。WA328資訊網(wǎng)——每日最新資訊28at.com

@Beanpublic WebClient slowServerClient() {    return WebClient.builder()            .baseUrl("http://127.0.0.1:8000")            .build();}@Beanpublic RouterFunction<ServerResponse> routes(WebClient slowServerClient) {    return route(GET("/"), (ServerRequest req) -> ok()            .body(                    slowServerClient                            .get()                            .exchangeToFlux(resp -> resp.bodyToFlux(Object.class)),                    Object.class            ));}

WebFlux 壓測(cè)結(jié)果如下:WA328資訊網(wǎng)——每日最新資訊28at.com

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

可以看到,WebFlux 的請(qǐng)求完全沒(méi)有阻塞,僅用了 25 個(gè)線程就達(dá)到了 964 req/sec 的吞吐。WA328資訊網(wǎng)——每日最新資訊28at.com

Tomcat+虛擬線程池

與平臺(tái)線程相比,虛擬線程的內(nèi)存占用量要低得多,運(yùn)行程序大量的創(chuàng)建虛擬線程,而不會(huì)耗盡系統(tǒng)資源;同時(shí)當(dāng)遇到 Thread.sleep(),CompletableFuture.await(),等待 I/O,獲取鎖時(shí),虛擬線程會(huì)自動(dòng)卸載,JVM 可以自動(dòng)切換到另外的等待就緒的虛擬線程,提升單個(gè)平臺(tái)線程的利用率,保證平臺(tái)線程不會(huì)浪費(fèi)在無(wú)意義的阻塞等待上。
WA328資訊網(wǎng)——每日最新資訊28at.com

要想使用虛擬線程,需要先在啟動(dòng)參數(shù)中加上 --enable-preview,同時(shí) Tomcat 在 10 版本已支持虛擬線程,我們只需要替換 Tomcat 的平臺(tái)線程池為虛擬線程池即可。WA328資訊網(wǎng)——每日最新資訊28at.com

@Beanpublic TomcatProtocolHandlerCustomizer<?> protocolHandler() {    return protocolHandler ->            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());}private final RestTemplate restTemplate;@GetMappingpublic ResponseEntity<Object> callSlowServer(){    return restTemplate.getForEntity("http://127.0.0.1:8000", Object.class);}

最終壓測(cè)結(jié)果如下:WA328資訊網(wǎng)——每日最新資訊28at.com

圖片圖片WA328資訊網(wǎng)——每日最新資訊28at.com

可以看到虛擬線程的壓測(cè)結(jié)果實(shí)際上與 WebFlux 的情況相同,但我們根本沒(méi)有使用任何復(fù)雜的響應(yīng)式編程技術(shù)。同時(shí)對(duì)慢速服務(wù)器的調(diào)用,也使用常規(guī)的阻塞  RestTemplate。我們所做的只是用虛擬線程執(zhí)行器替換線程池就達(dá)到更復(fù)雜的 Webflux 寫(xiě)法相同的效果。WA328資訊網(wǎng)——每日最新資訊28at.com

總的壓測(cè)結(jié)果如下:WA328資訊網(wǎng)——每日最新資訊28at.com

圖片WA328資訊網(wǎng)——每日最新資訊28at.com

通過(guò)以上壓測(cè)結(jié)果,我們可以得出以下結(jié)論:WA328資訊網(wǎng)——每日最新資訊28at.com

  • 傳統(tǒng)的線程池模式效果差強(qiáng)人意,可以通過(guò)提高線程數(shù)量可以提升吞吐,但是需要考慮到系統(tǒng)容量和資源限制,但是對(duì)于大部分場(chǎng)景來(lái)說(shuō)使用線程池去處理阻塞操作仍然是主流且不錯(cuò)的選擇。
  • WebFlux 的效果非常好,但是考慮到需要完全按照響應(yīng)式風(fēng)格進(jìn)行開(kāi)發(fā),成本及難度較大,同時(shí) WebFlux 與現(xiàn)有的一些主流框架存在一些兼容問(wèn)題,例如 Mysql 官方 IO 庫(kù)不支持 NIO、Threadlocal 兼容問(wèn)題等等。現(xiàn)有應(yīng)用的遷移基本要重寫(xiě)所有代碼,改動(dòng)量和風(fēng)險(xiǎn)都不可控。
  • 虛擬線程的效果非常好,最大的優(yōu)勢(shì)就是我們沒(méi)有修改代碼或采用任何反應(yīng)式技術(shù),唯一更改是將線程池替換為虛擬線程。雖然改動(dòng)較小,但與使用線程池相比,性能結(jié)果得到了顯著改善。

基于上述的壓測(cè)結(jié)果,可以較為樂(lè)觀的認(rèn)為虛擬線程會(huì)顛覆我們目前的服務(wù)和框架中的請(qǐng)求處理方法。WA328資訊網(wǎng)——每日最新資訊28at.com

六、總結(jié)

過(guò)去很長(zhǎng)時(shí)間,在編寫(xiě)服務(wù)端應(yīng)用時(shí),我們對(duì)于每個(gè)請(qǐng)求,都使用獨(dú)占的線程來(lái)處理,請(qǐng)求之間是相互獨(dú)立的,這就是 一請(qǐng)求一線程的模型這種方式易于理解和編程實(shí)現(xiàn),也易于調(diào)試和性能調(diào)優(yōu)。WA328資訊網(wǎng)——每日最新資訊28at.com

然而,一請(qǐng)求一線程風(fēng)格并不能簡(jiǎn)單地使用平臺(tái)線程來(lái)實(shí)現(xiàn),因?yàn)槠脚_(tái)線程是操作系統(tǒng)中線程的封裝。操作系統(tǒng)的線程會(huì)申請(qǐng)成本較高,存在數(shù)量上限。對(duì)于一個(gè)要并發(fā)處理海量請(qǐng)求的服務(wù)器端應(yīng)用來(lái)說(shuō),對(duì)每個(gè)請(qǐng)求都創(chuàng)建一個(gè)平臺(tái)線程是不現(xiàn)實(shí)的。在這種前提下,涌現(xiàn)出一批非阻塞 I/O 和異步編程框架,如 WebFlux ,RX-Java。當(dāng)某個(gè)請(qǐng)求在等待 I/O 操作時(shí),它會(huì)暫時(shí)讓出線程,并在 I/O 操作完成之后繼續(xù)執(zhí)行。通過(guò)這種方式,可以用少量線程同時(shí)處理大量的請(qǐng)求。這些框架可以提升系統(tǒng)的吞吐量,但是要求開(kāi)發(fā)人員必須熟悉所使用的底層框架,并按照響應(yīng)式的風(fēng)格來(lái)編寫(xiě)代碼,響應(yīng)式框架的調(diào)試?yán)щy,學(xué)習(xí)成本,兼容問(wèn)題使得大部分人望而卻步 。WA328資訊網(wǎng)——每日最新資訊28at.com

在使用虛擬線程之后,一切都將改變,開(kāi)發(fā)人員可以使用目前最習(xí)慣舒服的方式來(lái)編寫(xiě)代碼,高性能和高吞吐由虛擬線程自動(dòng)幫你完成,這極大地降低了編寫(xiě)高并發(fā)服務(wù)應(yīng)用的難度。WA328資訊網(wǎng)——每日最新資訊28at.com

參考文檔

  1. https://openjdk.org/jeps/444
  2. https://zhuanlan.zhihu.com/p/514719325
  3. https://www.vlts.cn/post/virtual-thread-source-code#%E5%89%8D%E6%8F%90
  4. https://zhuanlan.zhihu.com/p/499342616

本文鏈接:http://www.tebozhan.com/showinfo-26-17410-0.html虛擬線程原理及性能分析

聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com

上一篇: 好用的嵌入式設(shè)備日志輸出模塊 log.h

下一篇: 阿里Java面試官:CopyOnWriteArrayList底層是怎么保證線程安全的?

標(biāo)簽:
  • 熱門(mén)焦點(diǎn)
Top