命令式風(fēng)格編程一直深受開(kāi)發(fā)者喜愛(ài),如 if-then-else、while 循環(huán)、函數(shù)和代碼塊等結(jié)構(gòu)使代碼易理解、調(diào)試,異常易追蹤。然而,像所有好的東西一樣,通常也有問(wèn)題。這種編程風(fēng)格導(dǎo)致線(xiàn)程被阻塞時(shí)間遠(yuǎn)超過(guò)必要時(shí)間。
為了便于你理解,讓我們看一個(gè)典型的企業(yè)用例請(qǐng)求:
在像 Tomcat 這樣的應(yīng)用服務(wù)器中,一個(gè)平臺(tái)線(xiàn)程將專(zhuān)用于用戶(hù)請(qǐng)求,該線(xiàn)程將繼續(xù)調(diào)用從數(shù)據(jù)庫(kù)獲取數(shù)據(jù)的代碼(調(diào)用 FetchDataFromDB),然后調(diào)用從 Web 服務(wù)獲取數(shù)據(jù)的代碼(調(diào)用 FetchDataFromService),然后繼續(xù)合并并將數(shù)據(jù)發(fā)送給用戶(hù)(調(diào)用 SendDataToUser)。
如下圖,將執(zhí)行線(xiàn)程從上到下表示為一個(gè)垂直箭頭:
大多企業(yè)應(yīng)用都是 IO 綁定的,因此線(xiàn)程在大多時(shí)間內(nèi)實(shí)際是浪費(fèi)資源。
圖片
Java 中,平臺(tái)線(xiàn)程是昂貴資源,因?yàn)槟J(rèn),每個(gè)平臺(tái)線(xiàn)程消耗 1MB 棧內(nèi)存。即 JVM 中運(yùn)行的平臺(tái)線(xiàn)程數(shù)量有上限。因此,若一個(gè)平臺(tái)線(xiàn)程專(zhuān)用于用戶(hù)請(qǐng)求,對(duì)高并發(fā)用戶(hù)的應(yīng)用程序,就帶來(lái)問(wèn)題。傳統(tǒng)解決方案是創(chuàng)建一個(gè)具有最大線(xiàn)程數(shù)的線(xiàn)程池,并根據(jù)需要水平或垂直擴(kuò)展應(yīng)用程序:
為了提高性能,可用異步模型,并行運(yùn)行一些串行任務(wù)。如若假設(shè)數(shù)據(jù)庫(kù)和 Web 服務(wù)的獲取任務(wù)可以并行運(yùn)行,那么它們可以在各自的平臺(tái)線(xiàn)程中執(zhí)行。
圖片
用戶(hù)請(qǐng)求線(xiàn)程啟動(dòng)兩個(gè)線(xiàn)程:
在 Java 可通過(guò)向 Executor Service 提交 Callable 或 Runnable 任務(wù)并使用 Java Futures 來(lái)實(shí)現(xiàn)。
這將提高性能,因?yàn)閮蓚€(gè)數(shù)據(jù)獲取是并行執(zhí)行的。但是,即使在大多數(shù)時(shí)間內(nèi)可能會(huì)有性能提升,但是在短時(shí)間內(nèi),平臺(tái)線(xiàn)程的數(shù)量現(xiàn)在從 1 增加到 3。從可擴(kuò)展性看,在那段時(shí)間內(nèi)情況更壞。
響應(yīng)式編程設(shè)計(jì)就是為解決這問(wèn)題。
在于昂貴的平臺(tái)線(xiàn)程在阻塞操作期間浪費(fèi)大部分時(shí)間。隨 Servlet 3.0 和 3.1 引入,Servlet 線(xiàn)程在發(fā)送 HTTP 數(shù)據(jù)回用戶(hù)時(shí)無(wú)需保持活動(dòng)狀態(tài),這為更巧妙編程打開(kāi)解決線(xiàn)程阻塞的大門(mén)。Java 8 CompletableFuture類(lèi)可在其中創(chuàng)建響應(yīng)式管道。這種開(kāi)發(fā)風(fēng)格思想是為該用例指定一個(gè)執(zhí)行管道,而非執(zhí)行用例本身。
用戶(hù)請(qǐng)求線(xiàn)程只需指定用例的 CompletableFuture 管道(或任何其他管道),并在盡可能短的時(shí)間內(nèi)將其釋放回線(xiàn)程池(因?yàn)闊o(wú)需再保持活動(dòng)狀態(tài)以向用戶(hù)發(fā)送數(shù)據(jù))。
圖片
此時(shí),用戶(hù)請(qǐng)求線(xiàn)程創(chuàng)建一個(gè)運(yùn)行 3 個(gè)活動(dòng)的管道:
但創(chuàng)建此管道后,用戶(hù)請(qǐng)求線(xiàn)程將被簡(jiǎn)單釋放回線(xiàn)程池。大大減輕 JVM 負(fù)擔(dān),因?yàn)楝F(xiàn)在它有一個(gè)較少的線(xiàn)程要處理。一旦數(shù)據(jù)提取線(xiàn)程完成其執(zhí)行,數(shù)據(jù)將被發(fā)送給用戶(hù)。
但這只是部分解決問(wèn)題,因?yàn)閺?Web 服務(wù)、DB獲取數(shù)據(jù)的實(shí)際活動(dòng)仍是在它們各自的平臺(tái)線(xiàn)程中阻塞。這帶來(lái)問(wèn)題:SE須確保他從管道中生成的任務(wù)不是阻塞的。這很難做到,因?yàn)樗鞘謩?dòng)完成的,并且肯定是錯(cuò)誤的,因?yàn)樵诰幾g時(shí)或運(yùn)行時(shí)它不會(huì)被標(biāo)記為警告或錯(cuò)誤。
如何才能表現(xiàn)更好?達(dá)到更高標(biāo)準(zhǔn) OKR 呢?為使該設(shè)計(jì)完全響應(yīng)式,須以非阻塞方式獲取DB、Web 服務(wù)的數(shù)據(jù)。
作為 JDK 7 的一部分,NIO(New IO) 為非阻塞 IO 打開(kāi)大門(mén)。Java 所有基于 IO 類(lèi)和方法都有非阻塞版本了。如socket讀/寫(xiě)、文件讀/寫(xiě)、鎖 API 等。須使用這些類(lèi)/方法的非阻塞版本或支持 NIO 的庫(kù)來(lái)進(jìn)行數(shù)據(jù)的獲取。
圖片
每個(gè)獲取數(shù)據(jù)的 Fetch Data 中,發(fā)出請(qǐng)求的線(xiàn)程和獲取數(shù)據(jù)的線(xiàn)程不同,如:
這就是完全響應(yīng)式,它解決了關(guān)鍵問(wèn)題:IO 操作期間不阻塞。在這里使用平臺(tái)線(xiàn)程的唯一時(shí)間是在 CPU 操作期間,而不是在 IO 操作期間。在平臺(tái)線(xiàn)程的執(zhí)行部分已看不到任何紅色部分。
這種開(kāi)發(fā)風(fēng)格能實(shí)現(xiàn)應(yīng)用程序高可擴(kuò)展性。然而,解決方案過(guò)復(fù)雜。創(chuàng)建響應(yīng)式管道、調(diào)試它們及想象它們的執(zhí)行很困難。所以很正常,這種開(kāi)發(fā)風(fēng)格沒(méi)有流行開(kāi)來(lái),只有頂尖的開(kāi)發(fā)者才對(duì)此愛(ài)不釋手。Spring Boot 專(zhuān)門(mén)用于響應(yīng)式風(fēng)格編程的完整開(kāi)發(fā)技術(shù)棧即 Spring WebFlux,它使用 Project Reactor 庫(kù)提供了對(duì)DB、Web 服務(wù)等的非阻塞行為。
還有響應(yīng)式設(shè)計(jì)的其他替代方案嗎?當(dāng)然了!如今 Java 21 的發(fā)布,Oracle 推出備受期待的 Virtual Threads 功能。
平臺(tái)線(xiàn)程問(wèn)題是在阻塞操作期間,實(shí)際變得無(wú)用。平臺(tái)線(xiàn)程基本是os線(xiàn)程的簡(jiǎn)易包裝,畢竟os線(xiàn)程是昂貴的。
而虛擬線(xiàn)程是 JVM 中 Thread 類(lèi)的實(shí)現(xiàn),它是輕量級(jí)的。最終歸結(jié)為以下幾點(diǎn) — 當(dāng)使用虛擬線(xiàn)程進(jìn)行代碼執(zhí)行時(shí),它將在 CPU 操作期間使用平臺(tái)線(xiàn)程(稱(chēng)為載體線(xiàn)程),并且在遇到 IO 操作時(shí)將載體線(xiàn)程釋放。
虛擬線(xiàn)程中運(yùn)行時(shí),JVM 將自動(dòng)切換到使用 IO 操作的非阻塞版本。這種更改已在大多核心 Java 類(lèi)庫(kù)中為大多數(shù) IO 操作進(jìn)行了痛苦修改。當(dāng)代碼遇到 IO 操作,載體線(xiàn)程將被釋放,并且在該 IO 的數(shù)據(jù)可用時(shí),虛擬線(xiàn)程將被重新安排在另一個(gè)載體線(xiàn)程上處理數(shù)據(jù)。即在虛擬線(xiàn)程中阻塞不是問(wèn)題,因?yàn)榈讓拥妮d體線(xiàn)程被釋放了。
SE現(xiàn)在可選擇使用虛擬線(xiàn)程進(jìn)行用戶(hù)請(qǐng)求。即SE可繼續(xù)使用與以前相同的命令式開(kāi)發(fā)風(fēng)格,同時(shí)獲得使用響應(yīng)式管道時(shí)獲得的可擴(kuò)展性?xún)?yōu)勢(shì)(但沒(méi)有復(fù)雜性)。
這種方式在同步阻塞設(shè)計(jì)中的情況(注意,阻塞不是一個(gè)問(wèn)題)。
圖片
用戶(hù)請(qǐng)求線(xiàn)程是虛擬線(xiàn)程(藍(lán)色垂直箭頭)。線(xiàn)程上的紅色不再是問(wèn)題,因?yàn)樽枞僮髌陂g,底層的載體線(xiàn)程將被釋放,從而實(shí)現(xiàn)與使用響應(yīng)式框架相同的可擴(kuò)展性?xún)?yōu)勢(shì)。
阻塞在此也不再是問(wèn)題。前面提到可用 Java Futures 實(shí)現(xiàn),我們確實(shí)有這樣做的選擇。但Java 21引入 StructuredTaskScope 和 Subtask ,以處理結(jié)構(gòu)化異步行為。
圖片
虛擬線(xiàn)程 和 StructuredTaskScope 的組合將非常強(qiáng)大。虛擬線(xiàn)程使阻塞不再是一個(gè)問(wèn)題,而 StructuredTaskScope 將為我們提供更高級(jí)別的類(lèi),以直觀的方式處理異步編程。很難看到為什么還需要 Completable Futures。
隨著 Java 21 中 虛擬線(xiàn)程 引入,虛擬線(xiàn)程在阻塞狀態(tài)下不再是問(wèn)題。開(kāi)發(fā)人員:
即可創(chuàng)建高度可擴(kuò)展的應(yīng)用程序。替代方案是使用 Java 21 中引入的 虛擬線(xiàn)程 與 Java Futures 或 Structured Concurrency(Java 21 中的預(yù)覽功能) 類(lèi)的組合。
參考:
本文鏈接:http://www.www897cc.com/showinfo-26-55379-0.html響應(yīng)式編程又變天了?看JDK21虛擬線(xiàn)程如何顛覆!
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com