支持JDK19虛擬線程的web框架之:興風(fēng)作浪的ThreadLocal

推薦2年前發(fā)布 AI工具箱
39 00

關(guān)于ThreadLocal

  • 既然提到了線程,自然繞不開ThreadLocal類,它提供了線程本地變量,此變量和一般的變量不同。通過get & set 方法,每個線程可以獲取到自己獨立的變量。這個變量實例通常是私有且靜態(tài)的,可以存儲與線程相關(guān)的信息,如產(chǎn)品id、事務(wù)id等。
  • 下圖很形象的展現(xiàn)了ThreadLocal:是完全屬于每個線程自己的集合

虛擬線程中,ThreadLocal的問題

  • 既然每個線程都可以擁有屬于自己的ThreadLocal對象,那虛擬線程的情況又如何呢?
  • 虛擬線程的特性,使得我們可以在應(yīng)用代碼中創(chuàng)建成千上萬個虛擬線程去執(zhí)行并發(fā)任務(wù),而無需擔(dān)心線程數(shù)量對整體計算資源的負擔(dān),如果每個線程都用了ThreadLocal,那會不會出現(xiàn)成千上萬的ThreadLocal對象呢?線程是虛擬的,對象可是實實在在的,這樣會增加系統(tǒng)資源消耗,或者影響性能嗎?
  • 不過轉(zhuǎn)念一想,這么明顯的問題,連我都能想到,JDK組織又豈會漏掉?應(yīng)該是我多慮了吧,憑自己”豐富的經(jīng)驗”,我預(yù)測解決方案應(yīng)該和TLAB(Thread Local Allocation Buffer)類似,為海量虛擬線程的ThreadLoacal對象建立映射關(guān)系,做到高效管理
  • 然而現(xiàn)實很殘酷,臉,被狠狠地抽打,通過Oracle官方博客,知道實際情況真慘…,如下圖,中文注釋是我的解讀,極具悲觀色彩,如果翻譯得不準確請您告知,謝謝
  • 對上述內(nèi)容,個人理解是以下兩點:
  • 虛擬線程中使用ThreadLocal確實會帶來內(nèi)存問題,現(xiàn)在還無解,連虛擬線程自身的工程Loom都在自己代碼中刪除ThreadLocal的使用,那么我們普通用戶敢用嗎?還是避而遠之吧,在虛擬線程中不要用ThreadLocal
  • 編號429的JEP,為我們帶來了一個解決方案,一種名為Scoped values的變量,可以在一定范圍(scope)內(nèi)被訪問,至于這個scope,可以是一個內(nèi)存范圍(例如臨時變量就只能在方法內(nèi)),另外還有一種范圍被稱為dynamic scope,這個范圍就更加靈活了,不過這個JEP當前的狀態(tài)還很早期,如下圖,還在提案階段,這要是跳票了或者被否了,那我博客不就白寫了?就此打住吧,我不能再研究了
    • 搞清楚以上問題后,自己的八卦之心就控制不住了:既然虛擬線程上的ThreadLocal問題這么嚴重,放眼Java世界的生態(tài)這么繁榮,那么多框架和庫,那么…你們說
  • 有沒有哪個倒霉蛋掉進這個坑里去?
  • 慘不慘?
  • 從坑里爬出來沒有?
    • 你別說,還真有…

    踩坑勇士quarkus

    • 這位踩坑勇士,就是貫穿整個《支持JDK19虛擬線程的web框架》系列的quarkus,來吧,一起圍觀quarkus踩坑,順便學(xué)點知識
    • 先看quarkus官方文檔《virtual-threads.adoc》,如下圖
    • 我對上述內(nèi)容的理解:
  • quarkus的人發(fā)現(xiàn):傳統(tǒng)線程池模式改用虛擬線程后,性能提升明顯,但是反應(yīng)式框架改用虛擬線程后的提升并不明顯,而且還會帶來內(nèi)存消耗過大的問題(看過前面ThreadLocal分析的您,此刻應(yīng)該猜到原因了了,嘿嘿,您猜的沒錯)
  • 如果您的應(yīng)用對內(nèi)存有較嚴要求,quarkus官方建議您繼續(xù)堅持(stick)使用反應(yīng)式框架(這話中透露出濃濃的無可奈何,別催了,搞不定…)
    • 接下來官方就要甩鍋了,有趣的是,這次接鍋的并非JDK,而是大名鼎鼎的…Netty

    Netty的問題

    • 為什么是Netty接鍋呢?
    • 首先,Netty使用了Reactor線程模型,而Netty Reactor的核心是Event Loop,下圖來自《Netty in Action》,是處理web請求的內(nèi)部架構(gòu)圖,
    • 那么,應(yīng)該有多少個EventLoop線程呢?下圖是Netty源碼,默認值是CPU核數(shù)的2倍,看得出這是個很保守的數(shù)字
    • 從上面的架構(gòu)圖和代碼可以看出,Netty的反應(yīng)式框架的核心是使用少量線程來分發(fā)web請求,這樣的結(jié)果僅使用了少量線程資源就能高效處理事件
    • 也正式因為有了線程數(shù)不多這個前提,在對JSON做序列化處理時,Netty放心的使用了ThreadLocal,畢竟線程少,一個4核的CPU也才8個ThreadLocal,毫無壓力
    • 而且,為了更加高效,Netty還對ThreadLoacal進行過改造,也就是他們自研的FastThreadLocal
    • 然后,時間一天天過去,終于等來了JDK19發(fā)布,
    • quarkus的反應(yīng)式web服務(wù)模塊底層就是Netty,為了用上虛擬線程,他們動手了…咱們腦補一下吧,鋪天蓋地的虛擬線程線程,鋪天蓋地的FastThreadLocal對象,炸了吧您…Are U OK ?
    • 快樂之后,咱們還是要正視這個問題,表面上看是個坑,實際上是兩種設(shè)計思路的沖突:
  • 虛擬線程的特性類似golang的協(xié)程,很適合直接拿來處理高并發(fā)web請求,為每個請求分配一個虛擬線程,邏輯清晰直白,資源消耗又不高,典型的簡單高效
  • Netty的反應(yīng)式模型,核心思路就是用少量線程高效分發(fā)大量請求,本身就很高效,而且就算優(yōu)化,線程數(shù)也不是瓶頸
  • 所以,quarkus拎著虛擬線程沖到Netty的地盤一陣操作猛如虎,一看結(jié)果…唉,扯遠了,來看quarkus官方的解釋吧
    • 上圖紅框中那句話很有價值,咱們都能從中領(lǐng)悟到一些東西,我的收獲是:當線程數(shù)不是系統(tǒng)瓶頸的時候,就別沖動,強行上虛擬線程沒用

    quarkus強行挽尊

    • 既然虛擬線程不適合反應(yīng)式模型,個人認為:那就不妨大大方方的承認Netty的Reactor是優(yōu)秀的,放棄將虛擬線程加入進來,這樣不是挺好么?
    • 然而quarkus接下來的操作還是把我嚇到了:既然虛擬線程不適合反應(yīng)式模型?那就想辦法強行讓它適合,下圖就是quarkus的做法:在構(gòu)建階段,找到創(chuàng)建ThreadLocal的那段代碼,修改它的字節(jié)碼,以此來解決前面的內(nèi)存問題
    • 然后我就翻到了上圖提到的那段代碼
    • 好奇心驅(qū)使,我點開上圖那個NettyCurrentAdaptor去看了下源碼,當時就一陣頭暈眼花,ASM風(fēng)格的代碼您能撐多久?試試下圖
    • 按照官方的說法,經(jīng)過他們的優(yōu)化有百分之八十的提升,終于快要達到之前反應(yīng)式框架的水平了
    • 呃,搞得這么辛苦,也只是快要追上而已,那行,咱不用了行嗎?
    • 另外,上面說的優(yōu)化手段也不是默認開啟的,還要做以下幾步操作
  • maven的pom.xml添加以下依賴
  • io.quarkusquarkus-netty-loom-adaptor

  • 編譯構(gòu)建的時候,增加參數(shù)-Dnet.bytebuddy.experimental
  • 啟動的時候,增加參數(shù)–add-opens java.base/java.lang=ALL-UNNAMED
    • 上述操作算,quarkus的手段,我這個草根只能仰望,能開拓自己的見識:原來還可以這樣解決問題
    • 但我自己是絕對不敢模仿的,開玩笑,在編輯階段注入代碼,難度太大,并且后面如何維護和交接?
    ? 版權(quán)聲明

    相關(guān)文章

    暫無評論

    none
    暫無評論...