14 Java虛擬機器實現 synchronized
java 中的 synchronized 執行
在 Java 中,我們經常用 synchronized 關鍵字對程式進行加鎖。無論是一個程式碼塊還是靜態方法或者例項方法,都可以直接用 synchronized 宣告。
當宣告 synchronized 程式碼塊時,編譯的位元組碼將包含 monitorenter 和 monitorexit 指令。這兩種指令均會消耗運算元棧上的一個引用型別的元素,作為所要加鎖解鎖的鎖物件。
public void foo(Object lock) { synchronized (lock) { lock.hashCode(); } } // 上面的 Java 程式碼將編譯為下面的位元組碼 public void foo(java.lang.Object); Code: 0: aload_1 1: dup 2: astore_2 3: monitorenter 4: aload_1 5: invokevirtual java/lang/Object.hashCode:()I 8: pop 9: aload_2 10: monitorexit 11: goto19 14: astore_3 15: aload_2 16: monitorexit 17: aload_3 18: athrow 19: return Exception table: fromtotarget type 41114any 141714any
上述程式碼以及位元組碼中,包含了一個 monitorenter 指令以及多個 monitorexit 指令。這是因為 Java 虛擬機器需要確保所獲得的鎖在正常執行以及異常執行的路徑上都能夠被解鎖。
關於 monitorenter 和 monitorexit 的作用,可以抽象理解為每個鎖物件擁有一個鎖計數器和一個指向持有該鎖的執行緒的指標。
當執行 monitorenter 時,如果目標物件的計數器為 0,那麼說明它沒有加鎖。這個時候,Java 虛擬機器會將該鎖物件的持有執行緒設定為當前執行緒,並且將其計數器加 1。如果目標物件的計數器不為 0,判斷該鎖物件的持有執行緒是否為當前執行緒,如果說,則計數器加 1。否則需要等待,直至持有執行緒釋放該鎖。
當執行 monitorexit 時,Java 虛擬機器則需要將物件的計數器減 1。當計數器值為 0 時,代表該鎖已經被釋放掉了。
之所以採用這種計數器的方式,是為了允許同一執行緒重複獲取同一把鎖。例如:一個 Java 類中擁有多個 synchronized 方法,那麼這些方法之間互相呼叫,無論直接或間接,都會涉及對同一把鎖的重複加鎖操作。
接下來總結 HotSpot 虛擬機器中具體的鎖實現。
重量級鎖
重量級鎖是 Java 虛擬機器中最為基礎的鎖實現。這種情況下,Java 虛擬機器會阻塞加鎖失敗的執行緒,並且在目標鎖被釋放的時候,喚醒這些執行緒。
Java 執行緒的阻塞以及喚醒,都是依靠作業系統來完成的。這些操作涉及系統的呼叫,需要從作業系統的使用者態切換至核心態,其開銷非常之大。為了儘量避免昂貴的執行緒阻塞,喚醒操作,Java 虛擬機器會線上程進入阻塞狀態之前,以及被喚醒後競爭不到鎖的情況下,進入自旋狀態,在處理器上空跑並且倫旭鎖是否被釋放。與阻塞狀態相比,自旋狀態會浪費大量的處理器資源。
舉例:以等紅綠燈為例,Java 執行緒的阻塞相當於熄火停車,自旋狀態相當於怠速停車。如果紅燈時間長,那麼熄火停車更勝油。如果紅燈時間段,怠速停車更加適合。
對於 Java 虛擬機器來說,並不能看到紅燈的剩餘時間(不能明確知道執行緒保持自旋狀態多久可以加鎖)。這時,Java 虛擬機器給出可一種自適應的方案,根據以往自旋等待時是否獲得鎖,來動態調整自旋的時間。
舉例:上次沒熄火就等到了綠燈,這次就把怠速停車的時間設定久一點。上次沒熄火沒有等到綠燈,這次就把怠速停車時間設定短一點。
自旋狀態還有一個副作用,那便是不公平的鎖機制。處於阻塞狀態的執行緒,並沒有辦法立刻競爭被釋放的鎖。而處於自選狀態的執行緒,則可能有限獲得這把鎖。
輕量級鎖
深夜的十字路口,車輛來往很少,可能會出現一個路口一輛車在等紅綠燈,這樣的話車輛通行效率太低。於是,路口的燈設定成黃燈,過往車輛通過路口時注意避讓,最後保證依次通過。
Java 虛擬機器也存在類似的情形:多個執行緒在不同的時間段請求通一把鎖,不存在鎖競爭。針對這種情形,Java 虛擬機器採用了輕量級鎖,來避免重量級鎖的阻塞以及喚醒。
Java 虛擬機器是這樣區分輕量級鎖和重量級鎖的。在物件頭中的標記欄位,最後兩位用來表示該物件的鎖狀態。00 代表輕量級鎖,01 代表無鎖(或偏向鎖),10 代表重量級鎖,11 代表跟垃圾回收演算法的標記有關。
當加鎖時,Java 虛擬機器會判斷是否是重量鎖。如果不死,會在當前執行緒的當前棧幀中劃出一塊空間,作為鎖的鎖記錄,並且將鎖物件的標記欄位複製到該鎖記錄中。
之後,Java 虛擬機器會嘗試用 CAS 操作替換鎖物件的標記欄位。CAS 是一個原子操作,它會比較目標地址的值是否和期望值相等,如果相等,則替換為一個新的值。
舉例:當前鎖物件的標記欄位為 X-XYZ,Java 迅疾會比較該欄位是否為 X-X01。如果是,則替換為剛才分配的鎖記錄的地址。此時,該執行緒已成功獲得這把鎖,可以繼續執行了。如果不是 X-X01,那麼分兩種情況:第一,該執行緒重複獲取通一把鎖。此時,Java 虛擬機器會將鎖記錄清零,以代表該鎖被重複獲取。第二,其他執行緒持有該鎖。此時,Java 虛擬機器將把鎖膨脹為重量級鎖,並且阻塞當前執行緒。
當解鎖時,如果當前鎖記錄的值為 0,則代表重複進入同一把鎖,直接返回即可。否則,Java 虛擬機器會嘗試用 CAS 操作,比較鎖物件的標記欄位的值是否為當前鎖記錄的地址。如果是,則替換為鎖記錄中的值,也就是鎖物件原本的標記欄位。此時,該執行緒成功釋放這把鎖。如果不是,則意味著這把鎖已經膨脹為重量級鎖。此時 Java 虛擬機器會進入重量級鎖的釋放過程,喚醒因競爭該鎖二倍阻塞了的執行緒。
偏向鎖
輕量級鎖針對的是樂觀的情況,而偏向鎖針對就是更加樂觀的情況:從始至終只有一個執行緒請求某一把鎖。
如同紅路燈路口一直是紅燈,當看到你的車來的時候,紅燈才會變成綠燈,其他車一概都是紅燈,禁止通行。
當加鎖的時候,如果該鎖物件支援偏向鎖,那麼 Java 虛擬機器會通過 CAS 操作,將當前執行緒的地址記錄在鎖物件的標記欄位之中,並且標記欄位的最後三位設定為 101。
接下來的執行過程中,每當有執行緒請求這把鎖,Java 虛擬機器只需判斷鎖兌現標記的欄位中,最後三位是否為 101,是否包含當前執行緒的地址,以及 epoch 值是否和鎖物件的類的 epoch 值相同。如果滿足,那麼當前執行緒持有該偏向鎖,可以直接返回。
什麼是 epoch
先從偏向鎖的撤銷講起。當請求加鎖的執行緒和鎖物件標價欄位保持的執行緒地址不匹配時,Java 虛擬機器需要撤銷該偏向鎖。這個撤銷過程要求持有偏向鎖的執行緒到達安全點,再講偏向鎖替換成輕量級鎖。
如果某一類鎖物件的總撤銷數超過了一個閾值(相關引數的值為:20),那麼 Java 虛擬機器會宣佈這個類的偏向鎖失效。
具體的做法,在每一個類中維護一個 epoch 值,可以理解為第幾代偏向鎖。當設定偏向鎖時,Java 虛擬機器需要將該 epoch 值複製到鎖物件的標記欄位中。
在宣佈某個類的偏向鎖失效是,Java 虛擬機器實則將該類的 epoch 值加 1,表示之前那一代的偏向鎖已經失效。而新設定的偏向鎖則需要複製新的 epoch 值。
為了保證當前持有偏向鎖並且已加鎖的執行緒不至於因此丟鎖,Java 虛擬機器需要遍歷所有執行緒的 Java 棧,找出該類已加鎖的例項,並且將他們標記欄位中的 epoch 值加 1。該操作需要執行緒出遊安全點狀態。如果總撤銷數超過另一個閾值(40),此後的加鎖過程中直接為該類例項設定輕量級鎖。
總結
本文創作靈感來源於 極客時間 鄭雨迪老師的《深入拆解 Java 虛擬機器》課程,通過課後反思以及借鑑各位學友的發言總結,現整理出自己的知識架構,以便日後溫故知新,查漏補缺。
關注本人公眾號,第一時間獲取最新文章釋出,每日更新一篇技術文章。