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