synchronized鎖機制的實現原理


Synchronized 鎖機制的實現原理

Synchronized是Java種用於進行同步的關鍵字,synchronized的底層使用的是鎖機制實現的同步。在Java中的每一個對象都可以作為鎖。

Java中synchronized的兩個特性:

  1. 互斥性:即在同一時間內只允許同一個縣城持有某一個對象鎖,通過這種特性來實現多個線程中的協調機制,這樣在同一時間內只有一個線程對同步的代碼進行訪問,互斥性往往也被稱為原子性。

  2. 可見性:必須確保在獲取鎖的時候,線程內共享變量的值和主存一致,並且也必須保證在鎖在被釋放前,對共享變量所做的修改,對於隨后獲取鎖的另一個線程是可見的(即在獲取鎖時應該獲得的是最新的共享變量的值),否則另一個線程可能在本地緩存的某一個副本上繼續操作從而導致結果不一致。

synchronized鎖具體的三種形式:

  1. 對於普通同步方法,鎖對象是當前實例對象,進入同步代碼塊前需要獲得當前對象的鎖。
  2. 對於靜態同步方法,鎖的是當前類的對象,在Java中每一個類都有一個Class對象。
  3. 對於同步方法塊,鎖的是synchronized圓括號內的對象,這里的對象可以是一個普通的對象,也可以是一個Class對象,如果是Class對象的話,也就是所謂的類鎖了,而類鎖是通過類的Class對象實現的。

synchronized 的原理
 
JVM 基於進入和推出Monitor對象來實現方法和同步代碼塊,但兩者的實現細節不同。
 
同步代碼塊:使用monitorenter 和  monitorexit 指令實現。
synchronize修飾的方法並沒有 monitorenter 和 monitorexit  指令 ,而取代之的是 ACC_SYNCHRONIZED標識,該標志指明了該方法是一個同步方法,從而執行相應的同步調用。
 
Monitorenter 指令實現編譯后到同步代碼塊開始的位置,而 monitorexit 是插入在方法結束出和異常處,JVM要保證每個monitorter 必須要有一個與之對應的monitorexit 。任何一個對象都可以和一個monitor 相關聯,且當一個執行 monitorenter 指令的時候,會嘗試獲取synchronized 圓括號中對象中相關聯的 monitor的所有權,即嘗試獲取這個對象的鎖(這里的是重量級鎖,為沒有后面提到的偏向鎖和輕量級鎖)
 
下面來看一個具體的例子:
public class SynchronizedTest {
    
    public void readFile() throws IOException {
        synchronized(this) {
            System.out.println("同步代碼塊");
        }
    }
}

經過Javap 反編譯后,

C:\data>javap -c SynchronizedTest.class
Compiled from "SynchronizedTest.java"
public class test.test.a.SynchronizedTest {
  public test.test.a.SynchronizedTest();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public void readFile() throws java.io.IOException;
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: getstatic     #18                 // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #24                 // String 同步代碼塊
       9: invokevirtual #26                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: aload_1
      13: monitorexit
      14: goto          20
      17: aload_1
      18: monitorexit
      19: athrow
      20: return
    Exception table:
       from    to  target type
           4    14    17   any
          17    19    17   any
}

可以看出synchronized 同步語句塊的實現使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置。

 

下面的是同步方法:

public class SynchronizedTest {
    public synchronized void readFile() throws IOException {
        System.out.println("同步代碼塊");
    }
}

反編譯后如下圖所示:

 

 

 
java對象頭
 
在Hotpot 虛擬機中,對象頭主要包含兩部分數據:
 
Mark Word (標記字段)
Klass Point   (類型指針)
 
MarkWord: 默認存儲對象的HashCode,GC分代和鎖標志位信息。這些信息都與對象自身定義無關的數據,所以Mark Word被設計成非固定數據結構,以便在極小的空間存儲盡可能多的數據。(Mark Word的大小在32位虛擬機中占32個字節,在64位的虛擬機中占64個字節)。也就是說在運行期間Mark Word里面存儲的數據會隨着鎖標志的變化而變化。
Klass Point: 對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是那個類的實例。同樣的它的大小在32位虛擬機中占32個字節,在64位的虛擬機中占64個字節。
 

對於不同的對象頭它們的總結如下表:

長度 內容 說明
32/64bit MarkWord 存儲對象的hashCode,GC分代和鎖信息
32/64bit Klass Point 存儲到類元數據的指針
32/64bit Array Length 這個只針對數組對象而言,存儲數組的長度

Java對象頭中的MarkWord里面的存儲對象如下表:

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

在運行期間,Mark Word里存儲的數據會隨着鎖標志的變化而變化,它的變化如下表所示:

 

 

在64位的虛擬下,Mark Word是64bit的存儲結構,其存儲結構如下表:

鎖狀態 25bit 31bit 1bit 4bit 1bit 2bit
      cms_free 分代年齡 偏向鎖 鎖標志位
無鎖 unused hashCode     0 01
偏向鎖 ThreadId(54bit) Epoch(2bit)     1 01

對象頭的最后兩位存儲了鎖的標志,01是初始狀態表示無鎖,其對象頭里存儲的是對象的哈希嗎,隨着鎖級別的不同,對象頭中存儲的內容也會不同。偏向鎖存儲的當前占用此對象的線程ID;而輕量級鎖則是存儲指向線程棧中鎖記錄的指針。


Monitor監視器鎖

其中輕量級鎖和偏向鎖是Java6對synchronized鎖進行優化后增加的,我們稍后會進行分析。這里我們主要分析重量級鎖,也就是通常所說的synchronized對象鎖,鎖標識為10,其中指針指向monitor對象(也稱之為管程或者監視器鎖)的起始地址。每個對象都存在一個monitor與之關聯,對象與其monitor之間也存在着多種實現方式,如monitor可以與對象一起創建或者銷毀或當前線程試圖獲取鎖時自動生成,但一個monitor被某線程持有后,它便處於鎖定狀態。在Java虛擬機(HotSpot)中,monitor是有ObjectMonitor實現的,其主要數據結構如下(位於HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的)

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //記錄個數
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
}

 

ObjectMonitor 中有兩個隊列,_WaitSet 和 _EntryList ,用來保存ObjectWaiter 對象列表,每個等待鎖的線程都會被封裝成ObjectWaiter 對象,_owner 指向持有ObjectMonitor 對象的線程,當多個線程同時訪問同一同步代碼塊或者同步方法時,首先會進入 _EntryList 隊列,當線程獲取到monitor 后進入_Owner 區域並把 monitor中的 _Owner 變量設置為當前線程,同時monitor 中的計數器count 加1,若線程調用wait() 方法,將釋放當前持有的monitor,_owner變量恢復為null,count 自減 1 ,同時該線程進入_WaitSet 集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其他線程進入獲取monitor(鎖)。如下圖所示:

 

 

所以,monitor對象存在於每一個java對象的對象頭(存儲指針的指向),synchronized鎖便是通過這種方式獲取的,也是為什么java中任何對象都可以作為鎖的原因,同時也是 notify/notifyAll/wait 方法等存在於頂級對象Object中的原因。

 
 

原文鏈接:https://www.jianshu.com/p/3eda3d375e7e


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM