Java24解決了虛擬執行緒同步使用問題

banq發表於2024-11-24


在synchronized方法 + 虛擬執行緒 情況下會造成固定陷阱(Java虛擬執行緒不能使用同步synchronized鎖!),為避免這種陷阱,JEP 491改變了JVM對synchronized關鍵字的實現,這個提案是虛擬執行緒提出後的進一步深化。此更改已整合到 Java 24 中,並在最新的 JDK 24 EA 版本中可用。

背景
虛擬執行緒是Java 21透過JEP 444引入的輕量級執行緒,由JDK而非作業系統提供。虛擬執行緒顯著減少了開發、維護和觀察高吞吐量併發應用程式的工作量,使應用程式能夠使用大量執行緒。

虛擬執行緒的基本模型如下:

  • 執行緒必須被_排程_才能做有用的工作,即被分配到處理器核心上執行。對於平臺執行緒,JDK依賴於作業系統的排程器。對於虛擬執行緒,JDK有自己的排程器,它將虛擬執行緒分配給平臺執行緒,然後由作業系統像往常一樣排程。
  • 虛擬執行緒在執行阻塞操作(如I/O)時會解除安裝,當阻塞操作準備完成時,操作將虛擬執行緒重新提交給JDK的排程器,排程器將虛擬執行緒重新安裝到平臺執行緒上以繼續執行程式碼。


為何虛擬執行緒被固定到平臺執行緒?
在Java中,虛擬執行緒被固定(pinned)到平臺執行緒的原因主要與synchronized關鍵字的實現機制有關。

虛擬執行緒在synchronized方法內部執行程式碼時無法解除安裝。考慮以下synchronized從套接字讀取位元組的方法:

synchronized byte getData() {
    byte buf = ...;
    int nread = socket.getInputStream().read(buf);    <font>// Can block here<i>
    ...
}

期望:

  • 如果該read方法由於沒有可用位元組而阻塞,我們希望正在執行的虛擬執行緒getData從其載體上解除安裝。這將釋放一個平臺執行緒,以便 JDK 的排程程式可以在其上安裝不同的虛擬執行緒。

實際:

  • 不幸的是,由於getData是synchronized,JVM將正在執行的虛擬執行緒固定getData到其載體上。固定可防止虛擬執行緒解除安裝。
  • 因此,該read方法不僅會阻塞虛擬執行緒,還會阻塞其載體,從而阻塞底層作業系統執行緒,直到有位元組可供讀取。

1、原因:
Java中的synchronized關鍵字是基於監視器(monitors)實現的,每個物件都與一個監視器關聯,該監視器可以被獲取(鎖定)、持有一段時間,然後釋放(解鎖)。一次只能有一個執行緒持有物件的監視器。

在JVM中,哪個執行緒持有物件監視器的資訊是基於平臺執行緒(即作業系統執行緒)來跟蹤的,而不是基於虛擬執行緒。當虛擬執行緒執行synchronized例項方法並獲取與例項關聯的監視器時,JVM記錄的是虛擬執行緒的載體平臺執行緒持有監視器,而不是虛擬執行緒本身。

如果虛擬執行緒在synchronized方法中解除安裝,JDK的排程器可能會將另一個虛擬執行緒安裝到剛剛釋放的平臺執行緒上。

由於這個新虛擬執行緒的載體,JVM會認為它持有與例項關聯的監視器,這將破壞互斥性,因此JVM積極阻止虛擬執行緒在synchronized方法中解除安裝。

如果虛擬執行緒在執行synchronized方法時呼叫了Object.wait(),它會在JVM中阻塞直到被Object.notify()喚醒,並且載體重新獲取監視器。由於它在synchronized方法中執行,以及它的載體在JVM中被阻塞,所以虛擬執行緒也被固定pinned。

2、危害:
頻繁的固定(尤其是長時間固定)可能會損害可擴充套件性,導致飢餓甚至死鎖,因為沒有虛擬執行緒可以執行,因為所有平臺執行緒要麼被虛擬執行緒固定,要麼在JVM中被阻塞。

JEP 491目標
JEP 491旨在改變JVM對synchronized關鍵字的實現,允許虛擬執行緒在synchronized方法或語句中獲取、持有和釋放監視器,獨立於它們的載體。這將允許虛擬執行緒在被阻塞時解除安裝,釋放其載體平臺執行緒給JDK排程器,從而提高應用程式的可擴充套件性


JEP 491具體解決了哪些技術問題?
透過允許在synchronized方法和語句中阻塞的虛擬執行緒釋放其底層平臺執行緒,供其他虛擬執行緒使用,從而提高Java程式碼在使用synchronized時的可擴充套件性。

解決了虛擬執行緒在synchronized方法中不能解除安裝的問題,這個問題限制了能夠用於處理應用程式工作負載的虛擬執行緒數量。

同時,改進了診斷工具,以識別虛擬執行緒未能釋放平臺執行緒的情況,透過JDK Flight Recorder (JFR)記錄jdk.VirtualThreadPinned事件來識別程式碼中的問題區域。

由於synchronized關鍵字不再固定虛擬執行緒,因此不再需要jdk.tracePinnedThreads系統屬性,該屬性會在虛擬執行緒在synchronized方法中阻塞時列印堆疊跟蹤,但可能會引起效能問題。

解決了開發者在選擇synchronized和java.util.concurrent.locks包中的API時的困惑,現在可以根據實際需要選擇,而不必因為虛擬執行緒固定問題而被迫使用ReentrantLock。

JEP 491提議重新實現synchronized關鍵字,允許虛擬執行緒獲取、持有和釋放監視器,而不需要將這些操作與它們的載體執行緒繫結,解決了虛擬執行緒與監視器互動的問題。

本提案涉及對Object.wait()和Object.notify()機制的更改,允許虛擬執行緒在等待和重新獲取監視器時掛起和恢復,而不會鎖定其載體平臺執行緒。

本提案識別了一些虛擬執行緒仍然會固定的情況,例如在類初始化器內阻塞、等待其他執行緒初始化類以及在類載入期間解析符號引用時阻塞。

JEP 491提出了替代方案

  • 例如透過臨時擴充套件虛擬執行緒排程器的並行性來補償固定,但這種方法由於平臺執行緒數量有限而不具可擴充套件性。
  • 探討了在JVM載入時重寫每個類的位元組碼,用ReentrantLock替換synchronized的可能性,但這種方法會帶來高開銷和複雜性,並可能影響效能和與現有JVM特性的相容性。


 

相關文章