JVM與synchronized

JVM與synchronized

synchronized 關鍵字

使用:

  • 聲明一個 synchronized 代碼塊

  • 直接標記靜態方法或者實例方法

聲明 synchronized 代碼塊

public class T1 {

    public void foo(Object lock) {
        synchronized (lock) {
            lock.hashCode();
        }
    }
}

foo方法對應字節碼


//  public void foo(java.lang.Object);
//    descriptor: (Ljava/lang/Object;)V
//    flags: ACC_PUBLIC
//    Code:
//      stack=2, locals=4, args_size=2
//         0: aload_1
//         1: dup
//         2: astore_2
//         3: monitorenter
//         4: aload_1
//         5: invokevirtual #2                  // Method java/lang/Object.hashCode:()I
//         8: pop
//         9: aload_2
//        10: monitorexit
//        11: goto          19
//        14: astore_3
//        15: aload_2
//        16: monitorexit
//        17: aload_3
//        18: athrow
//        19: return

上面的字節碼中包含一個 monitorenter 指令以及多個 monitorexit 指令。這是因爲 Java 虛擬機需要確保所獲得的鎖在正常執行路徑,以及異常執行路徑上都能夠被解鎖

用 synchronized 標記方法

public class T2 {

    public synchronized void foo(Object lock) {
        lock.hashCode();
    }
}

foo方法對應字節碼

//  public synchronized void foo(java.lang.Object);
//    descriptor: (Ljava/lang/Object;)V
//    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
//    Code:
//      stack=1, locals=2, args_size=2
//         0: aload_1
//         1: invokevirtual #2                  // Method java/lang/Object.hashCode:()I
//         4: pop
//         5: return

上面字節碼中方法的訪問標記包括 ACC_SYNCHRONIZED。該標記表示在進入該方法時,Java 虛擬機需要進行 monitorenter 操作。而在退出該方法時,不管是正常返回,還是向調用者拋異常,Java 虛擬機均需要進行 monitorexit 操作

原理是通過方法調用指令檢查該方法在常量池中是否包含 ACC_SYNCHRONIZED 標記符,如果有,JVM 要求線程在調用之前請求鎖

這裏 monitorenter 和 monitorexit 操作所對應的鎖對象是隱式的。對於實例方法來說,這兩個操作對應的鎖對象是 this;對於靜態方法來說,這兩個操作對應的鎖對象則是所在類的 Class 實例

monitor監視器

  • 每個對象都有一個監視器,在同步代碼塊中,JVM通過monitorenter和monitorexist指令

  • 實現同步鎖的獲取和釋放功能

  • 當一個線程獲取同步鎖時,即是通過獲取monitor監視器進而等價爲獲取到鎖
    monitor的實現類似於操作系統中的管程

monitorenter指令

  • 每個對象都有一個監視器。當該監視器被佔用時即是鎖定狀態(或者說獲取監視器即是獲得同步鎖)。線程執行monitorenter指令時會嘗試獲取監視器的所有權,過程如下:

  • 若該監視器的進入次數爲0,則該線程進入監視器並將進入次數設置爲1,此時該線程即爲該監視器的所有者

  • 若線程已經佔有該監視器並重入,則進入次數+1

  • 若線程已經佔有該監視器並重入,則進入次數+1若其他線程已經佔有該監視器,則線程會被阻塞直到監視器的進入次數爲0,之後線程間會競爭獲取該監視器的所有權只有首先獲得鎖的線程才能允許繼續獲取多個鎖

monitorexit指令

  • 執行monitorexit指令將遵循以下步驟:

  • 執行monitorexit指令的線程必須是對象實例所對應的監視器的所有者

  • 指令執行時,線程會先將進入次數-1,若-1之後進入次數變成0,則線程退出監視器(即釋放鎖)

  • 其他阻塞在該監視器的線程可以重新競爭該監視器的所有權

具體實現過程

  • 在同步代碼塊中,JVM通過monitorenter和monitorexist指令實現同步鎖的獲取和釋放功能

  • monitorenter指令是在編譯後插入到同步代碼塊的開始位置

  • monitorexit指令是插入到方法結束處和異常處

  • JVM要保證每個monitorenter必須有對應的monitorexit與之配對

  • 任何對象都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態

  • 線程執行monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖

  • 線程執行monitorexit指令時,將會將進入次數-1直到變成0時釋放監視器

  • 同一時刻只有一個線程能夠成功,其它失敗的線程會被阻塞,並放入到同步隊列中,進入BLOCKED狀態

對象頭

JVM內存中的對象

圖示:

圖1.png

在JVM中,對象在內存中的佈局分成三塊區域:對象頭、示例數據和對齊填充

  • 對象頭: 對象頭主要存儲對象的hashCode、鎖信息、類型指針、數組長度(若是數組的話)等信息

  • 示例數據:存放類的屬性數據信息,包括父類的屬性信息,如果是數組的實例部分還包括數組長度,這部分內存按4字節對齊

  • 填充數據:由於JVM要求對象起始地址必須是8字節的整數倍,當不滿足8字節時會自動填充(因此填充數據並不是必須的,僅僅是爲了字節對齊)

對象頭:Hotspot虛擬機的對象頭主要包括兩部分數據

  • Mark Word(標記字段)

  • Klass Pointer(類型指針)

其中Klass Point是是對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例,Mark Word用於存儲對象自身的運行時數據,它是實現輕量級鎖和偏向鎖的關鍵

  • synchcronized的鎖是存放在Java對象頭中的

  • 如果對象是數組類型,JVM用3個子寬(Word)存儲對象頭,否則是用2個子寬

  • 在32位虛擬機中,1子寬等於4個字節,即32bit;64位的話就是8個字節,即64bit

Mark Word

對象頭中的標記字段(mark word)。它的最後兩位便被用來表示該對象的鎖狀態。其中,00 代表輕量級鎖,01 代表無鎖(或偏向鎖),10 代表重量級鎖,11 則跟垃圾回收算法的標記有關。

32位JVM的Mark Word的默認存儲結構(無鎖狀態)

鎖狀態 25bit 4bit 1bit是否是偏向鎖 2bit鎖標誌位
無鎖狀態 對象的hashCode 對象分代年齡 0 01

重量級鎖

重量級鎖是 Java 虛擬機中最爲基礎的鎖實現。在這種狀態下,Java 虛擬機會阻塞加鎖失敗的線程,並且在目標鎖被釋放的時候,喚醒這些線程。

爲了儘量避免昂貴的線程阻塞、喚醒操作,Java 虛擬機會在線程進入阻塞狀態之前,以及被喚醒後競爭不到鎖的情況下,進入自旋狀態,在處理器上空跑並且輪詢鎖是否被釋放。如果此時鎖恰好被釋放了,那麼當前線程便無須進入阻塞狀態,而是直接獲得這把鎖。

與線程阻塞相比,自旋狀態可能會浪費大量的處理器資源。這是因爲當前線程仍處於運行狀況,只不過跑的是無用指令。它期望在運行無用指令的過程中,鎖能夠被釋放出來。

我們可以用等紅綠燈作爲例子。Java 線程的阻塞相當於熄火停車,而自旋狀態相當於怠速停車。如果紅燈的等待時間非常長,那麼熄火停車相對省油一些;如果紅燈的等待時間非常短,比如說我們在 synchronized 代碼塊裏只做了一個整型加法,那麼在短時間內鎖肯定會被釋放出來,因此怠速停車更加合適

自旋狀態還帶來另外一個副作用,那便是不公平的鎖機制。處於阻塞狀態的線程,並沒有辦法立刻競爭被釋放的鎖。然而,處於自旋狀態的線程,則很有可能優先獲得這把鎖

輕量級鎖

深夜的十字路口,四個方向都閃黃燈的情況。由於深夜十字路口的車輛來往可能比較少,如果還設置紅綠燈交替,那麼很有可能出現四個方向僅有一輛車在等紅燈的情況。

因此,紅綠燈可能被設置爲閃黃燈的情況,代表車輛可以自由通過,但是司機需要注意觀察

Java 虛擬機也存在着類似的情形:多個線程在不同的時間段請求同一把鎖,也就是說沒有鎖競爭。針對這種情形,Java 虛擬機採用了輕量級鎖,來避免重量級鎖的阻塞以及喚醒。

偏向鎖

偏向鎖是Java 6之後加入的新鎖,它是一種針對加鎖操作的優化手段,經過研究發現,在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,因此爲了減少同一線程獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。

偏向鎖的核心思想是,如果一個線程獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變爲偏向鎖結構,當這個線程再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從而也就提供程序的性能。所以,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個線程申請相同的鎖。但是對於鎖競爭比較激烈的場合,偏向鎖就失效了,因爲這樣場合極有可能每次申請鎖的線程都是不相同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗後,並不會立即膨脹爲重量級鎖,而是先升級爲輕量級鎖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章