iT邦幫忙

2026 iThome 鐵人賽

0
Build on Google AI

把Google AI當夥伴打造個人高效Coding工作流系列 第 20 篇

第二十天Project Loom核心演進、結構化併發與傳統執行緒池淘汰考量

  • 分享至 

  • xImage
  •  

一、 傳統執行緒的致命瓶頸:Thread-per-Request 模型的極限
在 Java 21 之前,後端 Web 服務(如 Spring Boot + Tomcat)普遍採用「一個請求配置一個執行緒(Thread-per-Request)」模型:

  • 1:1 映射成本:Java 平台執行緒(Platform Thread)直接封裝作業系統核心執行緒(OS Thread)。
  • 記憶體壁壘:每個作業系統執行緒預設佔用約 1MB 的 Call Stack 記憶體,單台伺服器建立數千個執行緒即面臨記憶體耗盡。
  • CPU 浪費在阻塞等待:在微服務架構中,執行緒大部分時間都在等待資料庫查詢、Redis 回應或外部 HTTP API 呼叫。當 I/O 阻塞時,昂貴的 OS 執行緒被迫掛起閒置,嚴重壓低 CPU 利用率與整體系統吞吐量(Throughput)。
  • 反應式編程(Reactive Programming)的代價:過去為了壓榨吞吐量引入了 WebFlux、RxJava 等非同步框架,但代碼邏輯破碎(Callback Hell)、呼叫棧追蹤(Stack Trace)遺失、除錯困難度極高,大幅增加維護成本。

二、 Project Loom 核心革命:M:N 虛擬執行緒模型
Java 21正式發布的虛擬執行緒(JEP 444),目標是:用同步、直觀的Blocking代碼風格,達成非同步反應式的高併發吞吐量。

  • 極致輕量:
    • 虛擬執行緒是由JVM自行管理的使用者態執行緒,不是作業系統資源。
    • Stack記憶體最初只佔數百個Bytes,且存放在Java Heap堆積記憶體中,按需動態增減,不再受-Xss限制。
    • 單台普通伺服器可同時輕鬆運行 數十萬至數百萬個 虛擬執行緒。
  • 排程機制:
    • 底層由一組固定數量的載體執行緒(Carrier Thread,預設大小為CPU核心數的ForkJoinPool)負責運算。
    • 自動卸載:當虛擬執行緒執行Blocking I/O(例如socketRead()或 Thread.sleep())時,JVM會保存其呼叫棧狀態,將其從載體執行緒上拔除。
    • 釋放載體:原本的載體執行緒毫無停滯,立即轉去執行其他就緒的虛擬執行緒。
    • 自動掛載:當I/O事件完成(透過作業系統epoll/kqueue通知),JVM再將該虛擬執行緒重新掛載回任意空閒的載體執行緒上繼續執行,整個過程對開發者完全透明。

三、 避坑指南:虛擬執行緒的兩大致命陷阱
虛擬執行緒並非萬靈丹,在真實生產環境有兩個必須避開的架構雷區:

  1. 執行緒固定陷阱
    當虛擬執行緒在底層無法成功從載體執行緒上Unmount時,稱為Pinning(被釘死在OS執行緒上),導致載體執行緒被阻塞,退化成傳統OS執行緒的效能:
  • 觸發場景:
    • 在synchronized區塊或方法內部執行阻塞操作。
    • 呼叫了JNI本地原生C/C++函式庫或系統操作。
  • 工程避坑解法:
    • 將需要長時間I/O阻塞的臨界區,全面由synchronized改寫為JUC的 ReentrantLock(ReentrantLock 內部完全支援虛擬執行緒的正常調度與 Unmount)。
    • 啟動參數加上-Djdk.tracePinnedThreads=short,排查日誌中是否有Pinning警告。
  1. CPU 密集型運算無效
  • 虛擬執行緒的核心優勢在於化解I/O阻塞帶來的執行緒閒置。
  • 如果是高額的純CPU數學運算、影像編碼、雜湊爆破,虛擬執行緒完全無法提升運算效能,甚至會因為大量的排程切換反而略微降低效能。
  • 選型邊界:CPU密集任務仍應維持使用適當大小的傳統平台執行緒池。

五、 結構化併發與作用域值
Java 21 在預覽特性中引入了與虛擬執行緒配套的架構機制,徹底終結執行緒洩漏:

  1. 結構化併發
  • 傳統非同步多執行緒容易產生孤兒執行緒:子任務跑失敗了,其他並行子任務依然在背後空轉浪費資源。

  • 結構化概念:將派生出多個子任務、等待結果、聚合例外納入同一個代碼區塊結構中:
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Supplier userSubtask = scope.fork(() -> fetchUserData(id));
    Supplier orderSubtask = scope.fork(() -> fetchOrderData(id));

    scope.join(); // 同步等待兩個並行任務
    scope.throwIfFailed(); // 任一任務出錯即取消另一個並拋出例外

    // 聚合結果
    return new Dashboard(userSubtask.get(), orderSubtask.get());
    }

  1. 作用域值取代ThreadLocal
  • 傳統ThreadLocal每個變數都是可變的,且會一路複製繼承給子執行緒,當系統存在數百萬個虛擬執行緒時,ThreadLocal會造成極其嚴重的記憶體膨脹與洩漏風險。
  • ScopedValue具備單向不可變性、與嚴格的作用域生命週期綁定,能以極低開銷在數百萬個虛擬執行緒之間安全共享上下文(如 TenantId、TraceId)。

上一篇
第十九天ThreadPoolExecutor七大核心參數、工作流程與自訂飽和拒絕策略
下一篇
第二十一天Record語意、Sealed Classes代數型別與模式匹配完整實戰
系列文
把Google AI當夥伴打造個人高效Coding工作流 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言