性能怪獸——JDK19的虛擬線(xiàn)程
1.前言
生活在數(shù)字化時(shí)代的我們,在日常生活工作學(xué)習(xí)中或多或少遇到過(guò)這樣的問(wèn)題:雙十一購(gòu)物時(shí),提交訂單無(wú)法響應(yīng)或無(wú)法提交;查詢(xún)高考成績(jī)時(shí),網(wǎng)站打不開(kāi)或打開(kāi)了網(wǎng)站無(wú)法正常登錄查分;春運(yùn)高峰期,搶購(gòu)火車(chē)票時(shí),APP一直轉(zhuǎn)圈,卻搶不到票。
“性能”是每一個(gè)程序員在產(chǎn)品功能實(shí)現(xiàn)以后又愛(ài)又恨的話(huà)題。一款上線(xiàn)的產(chǎn)品,沒(méi)有經(jīng)過(guò)性能測(cè)試,猶如一顆定時(shí)炸彈,隨時(shí)會(huì)被引爆;有的性能問(wèn)題又如調(diào)皮的小孩,東躲西藏,等到了一定的時(shí)間就爆炸了。
而今在萬(wàn)物互聯(lián)的物聯(lián)網(wǎng)時(shí)代,隨著社會(huì)的進(jìn)步,數(shù)字化城市的建立,性能會(huì)更加凸顯它的重要性。面對(duì)各種各樣大的設(shè)備連接,面對(duì)大量設(shè)備的數(shù)據(jù)上報(bào),物聯(lián)網(wǎng)系統(tǒng)無(wú)時(shí)無(wú)刻不在承受著巨大的考驗(yàn)與壓力。
2.虛擬線(xiàn)程介紹
虛擬線(xiàn)程(Virtual Threads)就猶如名字一樣,并非傳統(tǒng)意義上的JAVA線(xiàn)程。
傳統(tǒng)意義上的JAVA線(xiàn)程(以下稱(chēng)為平臺(tái)線(xiàn)程)跟操作系統(tǒng)的內(nèi)核線(xiàn)程是一一映射的關(guān)系。而對(duì)于平臺(tái)線(xiàn)程的創(chuàng)建和銷(xiāo)毀所帶來(lái)的開(kāi)銷(xiāo)是非常大的,所以JAVA采用線(xiàn)程池的方式來(lái)維護(hù)平臺(tái)線(xiàn)程而避免線(xiàn)程的反復(fù)創(chuàng)建和銷(xiāo)毀。然而平臺(tái)線(xiàn)程也會(huì)占用內(nèi)存、CPU資源,往往在CPU和網(wǎng)絡(luò)連接成為系統(tǒng)瓶頸前,平臺(tái)線(xiàn)程首當(dāng)其沖的會(huì)成為系統(tǒng)瓶頸。在單臺(tái)服務(wù)器硬件資源確定的情況下,平臺(tái)線(xiàn)程的數(shù)量同樣也會(huì)因?yàn)橛布Y源而受到限制,也成為單臺(tái)服務(wù)器吞吐量提升的主要障礙。
而虛擬線(xiàn)程則是由JDK而非操作系統(tǒng)提供的一種線(xiàn)程輕量級(jí)實(shí)現(xiàn),它不依賴(lài)于平臺(tái)線(xiàn)程的數(shù)量,也不會(huì)增加額外的上下文切換開(kāi)銷(xiāo),也不會(huì)在代碼的整個(gè)生命周期中阻塞系統(tǒng)線(xiàn)程。整個(gè)虛擬線(xiàn)程的維護(hù)是通過(guò)JVM進(jìn)行管理,作為普通的JAVA對(duì)象存放在RAM中。那么意味著若干的虛擬線(xiàn)程可以在同一個(gè)系統(tǒng)線(xiàn)程上運(yùn)行應(yīng)用程序的代碼,只有在虛擬線(xiàn)程執(zhí)行的時(shí)候才會(huì)消耗系統(tǒng)線(xiàn)程,在等待和休眠時(shí)不會(huì)阻塞系統(tǒng)線(xiàn)程。
虛擬線(xiàn)程是一種非常廉價(jià)和豐富的線(xiàn)程,可以說(shuō)虛擬線(xiàn)程的數(shù)量是一種近乎于無(wú)限多的線(xiàn)程,它對(duì)硬件的利用率接近于最好,在相同硬件配置服務(wù)器的情況下,虛擬線(xiàn)程比使用平臺(tái)線(xiàn)程具備更高的并發(fā)性,從而提升整個(gè)應(yīng)用程序的吞吐量。如果說(shuō)平臺(tái)線(xiàn)程和系統(tǒng)線(xiàn)程調(diào)度為1:1的方式,虛擬線(xiàn)程則采用M:N的調(diào)度方式,其中大量的虛擬線(xiàn)程M在較少的系統(tǒng)線(xiàn)程N(yùn)上運(yùn)行。
那么虛擬線(xiàn)程是如何被JVM調(diào)度呢?首先創(chuàng)建一個(gè)虛擬線(xiàn)程,此時(shí)JVM會(huì)將虛擬線(xiàn)程裝載在平臺(tái)線(xiàn)程上,平臺(tái)線(xiàn)程則會(huì)去綁定一個(gè)系統(tǒng)線(xiàn)程。JVM會(huì)使用調(diào)度程序去使用調(diào)度線(xiàn)程執(zhí)行虛擬線(xiàn)程中的任務(wù)。任務(wù)執(zhí)行完成之后清空上下文變量,將調(diào)度線(xiàn)程返還至調(diào)度程序等待處理下一個(gè)任務(wù)。
3.虛擬線(xiàn)程VS平臺(tái)線(xiàn)程
虛擬線(xiàn)程的使用其實(shí)非常簡(jiǎn)單,跟平臺(tái)線(xiàn)程的使用方式基本相同,唯一不同的是創(chuàng)建虛擬線(xiàn)程時(shí),需要調(diào)用newVirtualThreadPerTaskExecutor()來(lái)創(chuàng)建虛擬線(xiàn)程。
以下我將三種線(xiàn)程創(chuàng)建的方式來(lái)模擬高并發(fā)IO,并打印系統(tǒng)線(xiàn)程數(shù),得到三種線(xiàn)程對(duì)處理10萬(wàn)累加計(jì)數(shù)的時(shí)長(zhǎng)。
? 主程序:
主程序采用一個(gè)定時(shí)任務(wù),每一秒打印一次所消耗的系統(tǒng)線(xiàn)程數(shù)。
第一種方式,無(wú)限制的使用普通線(xiàn)程(平臺(tái)線(xiàn)程),不需要考慮OOM的情況:
? 三次運(yùn)行結(jié)果:
普通線(xiàn)程(平臺(tái)線(xiàn)程)耗時(shí)(三次): 9584 ms 、10189ms、9586ms
普通線(xiàn)程(平臺(tái)線(xiàn)程)count計(jì)數(shù)為: 100000
初始占用系統(tǒng)線(xiàn)程數(shù):9;峰值占用系統(tǒng)線(xiàn)程線(xiàn)程數(shù):20027、19137、19140
第二種方式,使用線(xiàn)程池模式創(chuàng)建普通線(xiàn)程(平臺(tái)線(xiàn)程),考慮OOM的情況,線(xiàn)程池中創(chuàng)建1000普通線(xiàn)程:
? 三次運(yùn)行結(jié)果(由于運(yùn)行時(shí)間過(guò)長(zhǎng),無(wú)法完整截圖起始線(xiàn)程數(shù)):
線(xiàn)程池模式1000普通線(xiàn)程(平臺(tái)線(xiàn)程)耗時(shí)(三次): 100165ms 、100146ms、100159ms
線(xiàn)程池模式1000普通線(xiàn)程(平臺(tái)線(xiàn)程)count計(jì)數(shù)為: 100000
初始占用系統(tǒng)線(xiàn)程數(shù):9;峰值占用系統(tǒng)線(xiàn)程線(xiàn)程數(shù):1009、1009、1009
第三種方式,使用虛擬線(xiàn)程模式,創(chuàng)建10萬(wàn)個(gè)虛擬線(xiàn)程:
? 三次運(yùn)行結(jié)果:
- 虛擬線(xiàn)程耗時(shí)(三次): 2290ms、2523ms、2412ms
- 虛擬線(xiàn)程(平臺(tái)線(xiàn)程)count計(jì)數(shù)為: 100000
- 初始占用系統(tǒng)線(xiàn)程數(shù):9;峰值占用系統(tǒng)線(xiàn)程線(xiàn)程數(shù):16
由于JVM對(duì)系統(tǒng)線(xiàn)程的釋放機(jī)制,峰值占用系統(tǒng)線(xiàn)程數(shù)會(huì)逐漸從16降至9,由于釋放需要一定時(shí)間,沒(méi)對(duì)釋放系統(tǒng)線(xiàn)程進(jìn)行完整截圖。
由上表可見(jiàn),線(xiàn)程池模式處理10萬(wàn)累加并發(fā)處理的耗時(shí)是虛擬線(xiàn)程耗時(shí)的50倍;在不考慮服務(wù)內(nèi)存OOM的情況下,普通線(xiàn)程模式占用了大量系統(tǒng)線(xiàn)程處理10萬(wàn)累加并發(fā)耗時(shí)也是虛擬線(xiàn)程的5倍。虛擬線(xiàn)程只占用了7個(gè)系統(tǒng)線(xiàn)程,來(lái)處理10萬(wàn)累加并發(fā),這已經(jīng)不能用并發(fā)的巨大的性能提升來(lái)描述,而是并發(fā)怪獸,性能革命!但是虛擬線(xiàn)程的運(yùn)行速度并不比平臺(tái)線(xiàn)程快,所以不能用來(lái)降低延遲。
4.虛擬線(xiàn)程的使用場(chǎng)景
那么什么時(shí)候可以使用虛擬線(xiàn)程?
- 應(yīng)用系統(tǒng)有大量的并發(fā)任務(wù)(超過(guò)幾千個(gè)并發(fā)任務(wù)),這些任務(wù)也需要大量的時(shí)間等待;
- IO密集型場(chǎng)景,工作負(fù)載不受CPU限制。
如何改造當(dāng)前的線(xiàn)程池?
- 直接用虛擬線(xiàn)程代替線(xiàn)程池,如果代碼中使用CompletableFuture,則直接將異步執(zhí)行任務(wù)線(xiàn)程池替換為:Executors.newVirtualThreadPerTaskExecutor().
- 虛擬線(xiàn)程非常輕量化,不需要?jiǎng)?chuàng)建池,直接創(chuàng)建虛擬線(xiàn)程即可;
- synchronized更改為ReentrantLock減少固定到平臺(tái)線(xiàn)程的虛擬線(xiàn)程;
- 虛擬線(xiàn)程中ThreadLocal使用方式和平臺(tái)線(xiàn)程一致,但創(chuàng)建了大量的虛擬線(xiàn)程,每個(gè)虛擬線(xiàn)程中均有ThreadLocal實(shí)例及其引用的數(shù)據(jù),則會(huì)對(duì)內(nèi)存帶來(lái)很大的負(fù)擔(dān)。
5.總結(jié)
在萬(wàn)物互聯(lián)的今天,物聯(lián)網(wǎng)平臺(tái)日益增長(zhǎng)的設(shè)備連接數(shù)和龐大的并發(fā)量已經(jīng)不是我們能忽視的問(wèn)題,JDK19中的性能怪獸--虛擬線(xiàn)程給我們帶來(lái)了一個(gè)嶄新的方向來(lái)解決物聯(lián)網(wǎng)平臺(tái)并發(fā)量的問(wèn)題。虛擬線(xiàn)程中還有很多可以深挖和學(xué)習(xí)與借鑒的前沿技術(shù)和設(shè)計(jì)思想,這需要我們不斷的探究和實(shí)踐來(lái)提升我們的OneNET平臺(tái),以應(yīng)對(duì)未來(lái)無(wú)限的機(jī)遇與挑戰(zhàn)。