synchronized鎖的實現原理及鎖升級

synchronized同步

        Java中每一個對象都可以作爲鎖。具體表現爲以下3種形式:

  1. 普通同步方法,鎖是當前實例對象

  2. 靜態同步方法,鎖是當前類的class對象

  3. 同步方法塊,鎖是synchronized括號裏配置的對象

          當一個線程訪問同步代碼塊時,它首先是需要得到鎖才能執行同步代碼,當退出或者拋出異常時必須要釋放鎖,那麼它是如何來實現這個機制的呢?我們先看一段簡單的代碼:

public class SyncTest {
	public void syncBlock() {
		synchronized (this) {
			System.out.println("hello block");
		}
	}
	public synchronized void syncMethod() {
		System.out.println("hello method");
	}
}

        當SyncTest.java被編譯成class文件的時候,synchronized關鍵字和synchronized方法的字節碼略有不同,我們可以用javap -v 命令查看class文件對應的JVM字節碼信息,部分信息如下: 

........省略代碼........
{
  public SyncTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public void syncBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String hello block
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any
      LineNumberTable:
        line 3: 0
        line 4: 4
        line 5: 12
        line 6: 22
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 17
          locals = [ class SyncTest, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public synchronized void syncMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String hello method
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 8: 0
        line 9: 8
}

從上面的中文註釋處可以看到,

同步代碼塊

        對於synchronized關鍵字而言,javac在編譯時,會生成對應的monitorenter和monitorexit指令分別對應synchronized同步塊的進入和退出,有兩個monitorexit指令的原因是:爲了保證拋異常的情況下也能釋放鎖,所以javac爲同步代碼塊添加了一個隱式的try-finally,在finally中會調用monitorexit命令釋放鎖。

同步方法:

        而對於synchronized方法而言,javac爲其生成了一個ACCSYNCHRONIZED關鍵字,在JVM進行方法調用時,發現調用的方法被ACCSYNCHRONIZED修飾,則會先嚐試獲得鎖。

對象頭

 synchronized用的鎖是存在Java對象頭裏的,對象頭如下圖

  • Mark Word:存儲了對象的hashcode 以及鎖信息、GC年齡等。
  • Class Metadata Address:存儲類的元信息--Class對象的指針(Class對象在方法區中)。
  • Array length:如果數組的話,還會再存儲下數組的長度。

在運行期間,MarkWord裏存儲的數據會隨着鎖標誌位的變化而變化,Mark Word可能變化爲以下4中數據

說明:

  • MarkWord 中包含對象 hashCode 的那種無鎖狀態是偏向機制被禁用時, 分配出來的無鎖對象MarkWord 起始狀態。(在這種狀態下,要鎖定向的話就是輕量級鎖)可以通過-XX:-UseBiasedLocking參數來啓動偏向鎖
  • 偏向機制被啓用時,分配出來的對象狀態是 ThreadId|Epoch|age|1|01, ThreadId 爲空時標識對象尚未偏向於任何一個線程, ThreadId 不爲空時, 對象既可能處於偏向特定線程的狀態, 也有可能處於已經被特定線程佔用完畢釋放的狀態, 需結合 Epoch 和其他信息判斷對象是否允許再偏向(rebias)。

重量級鎖

重量級鎖是我們常說的傳統意義上的鎖,其利用操作系統底層的同步機制(線程阻塞需要從用戶態切換到核心態)去實現Java中的線程同步。

重量級鎖的狀態下,對象的mark word爲指向一個堆中monitor對象的指針

Monitor 有兩個隊列 WaitSet 和 _EntryList,存儲ObjectWaiter列表(所有等待的線程都會被包裝成ObjectWaiter);

① 線程申請owner Monitor對象,首先會被加入到 _EntryList ;

② 線程申請owner Monitor對象,進入到 Owner區域,此時count +1;

③線程調用wait方法,釋放鎖,進入到 WaitSet ,此時count -1

④ 線程再次申請owner

⑤ 線程處理完畢後釋放資源並退出。

輕量級鎖

        輕量級鎖並不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗

輕量級鎖加鎖

1、在代碼進入同步塊的時候,JVM首先會在當前線程的棧幀中建立一個名爲鎖記錄的空間(LockRecord),用於存儲鎖對象頭中目前的MarkWord的拷貝,官方稱之爲Displaced Mark Word,這時候線程堆棧與對象頭的狀態如下圖

2、然後虛擬機使用CAS(CompareAndSwap原子操作)嘗試將對象的MarkWord更新爲指向LockRecord的指針

2.1、如果更新成功,那麼這個線程就擁有了該對象的鎖,並且對象MarkWord的鎖標誌變爲00,表示對象處於輕量級鎖定狀態,如下圖

2.2、如果更新失敗,JVM會先檢查對象的MarkWord是否指向當前線程的棧楨,如果是則說明當前線程已經擁有了這個對象的鎖(鎖重入),就可以繼續進入同步塊繼續執行。否則說明該鎖對象已經被其他線程搶佔了,當前線程變嘗試使用自旋來獲取鎖。如果自旋獲取鎖成功,則繼續以輕量級鎖進入同步塊;否則,(1)鎖膨脹,將當前鎖對象頭的MarkWord修改成重量級鎖、(2)該線程進入阻塞狀態。 

輕量級鎖解鎖

輕量級解鎖時,使用原子的CAS操作將DisplacedMarkWord替換到對象頭MarkWord中,

如果替換成功,則表示沒有發生鎖競爭,結束。

如果失敗,說明有其他線程嘗試過獲取鎖(並且將鎖對象膨脹爲重量級鎖),存在鎖競爭,釋放鎖並喚醒被掛起的線程。

整體流程圖如下圖:(解鎖過程中,對象膨脹爲重量級鎖的具體細節不太清楚,鎖對象的MarkWord中如何指向了其他線程創建的那個重量級鎖)

偏向鎖

偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖。

獲取鎖

case 1:當該對象第一次被線程獲得鎖的時候,發現是匿名偏向狀態(下圖中的左分支),則會用CAS指令,將mark word中的thread id由0改成當前線程Id。如果成功,則代表獲得了偏向鎖,繼續執行同步塊中的代碼。否則,將偏向鎖撤銷,升級爲輕量級鎖(下圖中的右邊分支)。

case 2.當其他線程進入同步塊時,發現已經有偏向的線程了,偏向鎖模式宣告失敗,會進入到撤銷偏向鎖的邏輯裏,一般來說,會在安全點safepoint暫停擁有偏向鎖的線程,檢查偏向的線程是否還存活,

如果偏向的線程已經不存活或者不在同步塊中(中間上面指向右邊的箭頭),則將對象頭的mark word改爲無鎖狀態(unlocked),之後再升級爲輕量級鎖。

如果存活且還在同步塊中則將鎖升級爲輕量級鎖(中間下面指向右邊的箭頭),原偏向的線程繼續擁有鎖,當前線程則走入到鎖升級的邏輯裏;當前線程進行自旋獲取鎖,如果成功獲得資源(即之前獲的資源的線程執行完成並釋放了共享資源),則整個狀態依然處於輕量級鎖的狀態,如果自旋失敗,則進入重量級鎖的狀態,這個時候,自旋的線程進行阻塞,等待之前線程執行完成並喚醒自己。

鎖的優缺點對比

優點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要執行額外的消耗,就是直接操作鎖對象頭的MarkWord,和執行非同步相比僅存在納秒級的差距 如果線程間存在鎖競爭,則帶來額外的鎖撤銷的消耗 適用於只有一個線程訪問同步塊場景
輕量級鎖

使用CAS消除互斥量

競爭的線程不會阻塞,提高了程序的響應速度。

採用自旋,不需要因阻塞而從用戶態切換到核心態。

使用自旋會消耗CPU

追求響應速度

同步塊執行速度非常快

重量級鎖 線程競爭不使用自旋,不會消耗CPU 線程阻塞,響應時間緩慢

追求吞吐量

同步塊執行時間較長

參考死磕Synchronized底層實現

死磕 Java 併發:深入分析 synchronized 的實現原理 

java 偏向鎖

Java中的偏向鎖,輕量級鎖, 重量級鎖解析

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