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