淺析Java中的虛擬線程
在最近發(fā)布的JDK 21 LTS版本中,加入了許多新特性。其中對我們開發(fā)人員影響最大的應(yīng)該是分代ZGC以及Java的虛擬線程。在本篇文章中,我將帶大家深入了解Java虛擬線程的原理、如何使用、使用的注意事項(xiàng)以及其他相似技術(shù)的差別。
什么是虛擬線程
首先,我們需要了解什么是虛擬線程。
在平時(shí)的開發(fā)過程中,我們所使用的多線程往往意味著平臺線程。平臺線程代表著JVM直接與操作系統(tǒng)交互,創(chuàng)建了一個(gè)一個(gè)的線程,并且在JVM中還要為這個(gè)線程單獨(dú)開辟內(nèi)存使用。一般在JVM中創(chuàng)建一個(gè)平臺線程,開銷大約在1M左右。為了避免創(chuàng)建線程和銷毀線程帶來的巨大開銷,我們通常選擇使用池化技術(shù)來維護(hù)一些活躍的線程。
此外,線程的數(shù)量也需要嚴(yán)格控制。如果在一個(gè)線程池中維護(hù)成千上百個(gè)線程,往往效率并不盡如人意。因?yàn)榫€程在切換時(shí)涉及到CPU上下文的切換,如果線程數(shù)過多,反而會降低執(zhí)行效率。因此,如何控制線程池的大小也是考驗(yàn)工程師經(jīng)驗(yàn)的難點(diǎn)。
平臺線程與系統(tǒng)線程關(guān)系如下
為了解決這個(gè)問題,虛擬線程應(yīng)運(yùn)而生,虛擬線程并不是Java的首創(chuàng),它在很多其他語言中被稱為協(xié)程、纖程、綠色線程、用戶態(tài)線程等,虛擬線程相對平臺線程,并不直接與操作系統(tǒng)交互,虛擬線程的數(shù)據(jù)是維護(hù)在堆內(nèi)存中,由JVM創(chuàng)建的平臺線程來持有,由平臺線程來決定什么時(shí)候來切換虛擬線程,大概圖如下
雖然圖中只畫了幾個(gè)虛擬線程,但是在實(shí)際使用中,我們可以創(chuàng)建成百上千的虛擬線程而不用擔(dān)心資源消耗的問題
首先原因在于虛擬線程的開銷極其廉價(jià),一個(gè)虛擬線程可能才使用幾百字節(jié),所以幾遍創(chuàng)建成百上千也不會消耗太多內(nèi)存資源
其次雖然我們有極多的虛擬線程,但是實(shí)際上執(zhí)行線程依舊只有幾個(gè)平臺線程,所以在線程使用中不會由于CPU上下文的切換導(dǎo)致的額外開銷。
使用
接下來我們用代碼來實(shí)踐一下虛擬線程,JDK的工程師為了方便我們快速進(jìn)行虛擬線程的升級,可以使用Thread來快速創(chuàng)建虛擬線程
Thread vt = Thread.startVirtualThread(() -> { Thread.sleep(1000); });
這種方法創(chuàng)建的虛擬線程會立刻啟動,那么如果我們想創(chuàng)建一個(gè)需要手動啟動的虛擬線程,可以參考如下的方式
// 創(chuàng)建VirtualThread Thread.ofVirtual().unstarted(() -> { Thread.sleep(1000); }); // 運(yùn)行 vt.start();
還可以創(chuàng)建虛擬線程的工廠來使用
// 創(chuàng)建ThreadFactory: ThreadFactory tf = Thread.ofVirtual().factory(); // 創(chuàng)建VirtualThread: Thread vt = tf.newThread(() -> { Thread.sleep(1000); }); // 運(yùn)行: vt.start();
我們將線程的上下文信息打印出來看看
VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1
從這個(gè)打印內(nèi)容可以大概看出,虛擬線程底層采用了ForkJoinPool來維護(hù)平臺線程,而后面的worker-1則代表具體的平臺線程的名稱,前面的#21代表當(dāng)前虛擬線程的數(shù)量,那么可能有同學(xué)就會問了 那如果我全局有成千上百個(gè)虛擬線程,我怎么知道是哪塊業(yè)務(wù)的線程除了問題呢。
我們可以對線程工廠進(jìn)行命名,使用該線程工廠命名后,在線程上下文中就會打印對應(yīng)虛擬線程工廠的信息。
ThreadFactory factory = Thread.ofVirtual().name("myVirtual").factory(); try(ExecutorService executor = Executors.newThreadPerTaskExecutor(factory)) { for (int i=0; i<100000; i++) { // 也可以直接傳入Runnable或Callable: executor.submit(() -> { System.out.println(Thread.currentThread()); Thread.sleep(1000); return true; }); } }
VirtualThread[#23,myVirtual]/runnable@ForkJoinPool-1-worker-2
VirtualThread[#275,myVirtual]/runnable@ForkJoinPool-1-worker-7
VirtualThread[#282,myVirtual]/runnable@ForkJoinPool-1-worker-5
VirtualThread[#285,myVirtual]/runnable@ForkJoinPool-1-worker-4
VirtualThread[#288,myVirtual]/runnable@ForkJoinPool-1-worker-3
VirtualThread[#234,myVirtual]/runnable@ForkJoinPool-1-worker-10
從日志中不難看出,我們已經(jīng)成功在線程上下文標(biāo)記了這一塊虛擬線程屬于哪個(gè)工廠,并且通過信息后綴中的worker-10可以看出底層的ForkJoinPool已經(jīng)創(chuàng)建了10個(gè)平臺線程來操作虛擬線程,因?yàn)槲疫@臺電腦是10核的,所以底層的線程池會創(chuàng)建與核心數(shù)相等的線程來操作虛擬線程,在我的實(shí)踐中發(fā)現(xiàn),無論你創(chuàng)建多少調(diào)度器,所有底層的虛擬線程都是由同一個(gè)平臺線程池來操控的。
而且在示例代碼中,我創(chuàng)建了十萬個(gè)虛擬線程,如果我是用的是平臺線程,且不說執(zhí)行效率,但是內(nèi)存分配就已經(jīng)使項(xiàng)目OOM了。
注意事項(xiàng)
ThreadLocal支持
有同學(xué)可能會問,在虛擬線程中能否使用ThreadLocal對象呢?官方給出的答案是,能夠使用但不建議。在過去,我們在維護(hù)線程池時(shí),線程的數(shù)量是固定的且相對較少。我們通過手動清理的方式來重用ThreadLocal對象。然而,在使用虛擬線程后,我們不再關(guān)注具體的線程數(shù),這導(dǎo)致ThreadLocal對象的數(shù)量無法控制,從而占用了額外的內(nèi)存。需要注意的是,虛擬線程與平臺線程不共享同一個(gè)ThreadLocal。
永遠(yuǎn)不要池化虛擬線程
之前我們提到過,虛擬線程的開銷非常低,每個(gè)虛擬線程可能只消耗幾百字節(jié)的內(nèi)存。這意味著我們不需要為虛擬線程創(chuàng)建所謂的線程池。相應(yīng)地,根據(jù)我們之前的編碼習(xí)慣,如果某個(gè)功能只能接受20個(gè)并發(fā)請求,我們可能會創(chuàng)建一個(gè)固定大小為20的線程池來進(jìn)行限流。然而,如果使用虛擬線程,我們應(yīng)盡量改用類似信號量的方式來實(shí)現(xiàn)。
協(xié)程、IO多路復(fù)用與虛擬線程的關(guān)系
有很多同學(xué)可能對對這幾個(gè)概念相對比較混淆,我們可以來梳理下這幾個(gè)概念之間的異同,首先協(xié)程與虛擬線程從概念上是相同的,只是不同語言之間的實(shí)現(xiàn)方式是不同的,都是在用戶態(tài)線程中維護(hù)多個(gè)子線程進(jìn)行切換,只是如Python語言可能需要手動去喚起協(xié)程,而Java中的虛擬線程都是交由JVM調(diào)度的。
而IO多路復(fù)用,我們以Netty為例,它內(nèi)部使用了IO多路復(fù)用技術(shù)來管理和處理大量的并發(fā)IO操作。Netty的IO多路復(fù)用機(jī)制可以有效地管理和調(diào)度多個(gè)連接,提高系統(tǒng)的吞吐量和性能。它基于事件驅(qū)動的設(shè)計(jì)模型,通過選擇器(Selector)來同時(shí)監(jiān)控多個(gè)IO通道的狀態(tài),當(dāng)有IO事件就緒時(shí),通過回調(diào)機(jī)制來進(jìn)行處理。而虛擬線程通常用于解決IO阻塞的問題,通過在IO操作或時(shí)間等待點(diǎn)上主動釋放執(zhí)行權(quán),來實(shí)現(xiàn)更好的并發(fā)性能。虛擬線程可以減少線程切換的開銷,但它仍然是在應(yīng)用程序內(nèi)部執(zhí)行的,并不能直接替代IO多路復(fù)用來管理和處理大量的并發(fā)IO操作??梢哉f術(shù)業(yè)有專攻,但是虛擬線程確實(shí)會對多IO操作有效率提升的。
到此這篇關(guān)于淺析Java中的虛擬線程的文章就介紹到這了,更多相關(guān)Java虛擬線程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot2零基礎(chǔ)到精通之?dāng)?shù)據(jù)與頁面響應(yīng)
SpringBoot是一種整合Spring技術(shù)棧的方式(或者說是框架),同時(shí)也是簡化Spring的一種快速開發(fā)的腳手架2022-03-03MyBatis-Plus 分頁查詢以及自定義sql分頁的實(shí)現(xiàn)
這篇文章主要介紹了MyBatis-Plus 分頁查詢以及自定義sql分頁的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08Spring Eureka 未授權(quán)訪問漏洞修復(fù)問題小結(jié)
項(xiàng)目組使用的 Spring Boot 比較老,是 1.5.4.RELEASE ,最近被檢測出 Spring Eureka 未授權(quán)訪問漏洞,這篇文章主要介紹了Spring Eureka 未授權(quán)訪問漏洞修復(fù)問題小結(jié),需要的朋友可以參考下2024-04-04Java設(shè)計(jì)模式之工廠模式實(shí)現(xiàn)方法詳解
這篇文章主要介紹了Java設(shè)計(jì)模式之工廠模式實(shí)現(xiàn)方法,結(jié)合實(shí)例形式較為詳細(xì)的分析了工廠模式的分類、原理、實(shí)現(xiàn)方法與相關(guān)注意事項(xiàng),需要的朋友可以參考下2017-12-12SpringCloud OpenFeign與Ribbon客戶端配置詳解
在springcloud中,openfeign是取代了feign作為負(fù)載均衡組件的,feign最早是netflix提供的,他是一個(gè)輕量級的支持RESTful的http服務(wù)調(diào)用框架,內(nèi)置了ribbon,而ribbon可以提供負(fù)載均衡機(jī)制,因此feign可以作為一個(gè)負(fù)載均衡的遠(yuǎn)程服務(wù)調(diào)用框架使用2022-11-11淺談Java中SimpleDateFormat 多線程不安全原因
SimpleDateFormat是Java中用于日期時(shí)間格式化的一個(gè)類,本文主要介紹了淺談Java中SimpleDateFormat 多線程不安全原因,感興趣的可以了解一下2024-01-01