Synchronized原理

foofoo發表於2018-09-06

1.Syncronized應用

synchronized是java中加鎖的關鍵字,可以用來給物件和方法或者程式碼塊加鎖,當它鎖定一個方法或者一個程式碼塊的時候,同一時刻最多隻有一個執行緒可以執行這段程式碼。另一個執行緒必須等待當前執行緒執行完這個程式碼塊以後才能執行該程式碼塊。然而,當一個執行緒訪問object的一個加鎖程式碼塊時,另一個執行緒仍可以訪問該object中的非加鎖程式碼塊。

1.1 三種應用方式

synchronized關鍵字最主要有以下3種應用方式,下面分別介紹

修飾例項方法:作用於當前例項加鎖,進入同步程式碼前要獲得當前例項的鎖

public class AccountingSync implements Runnable{
    //共享資源(臨界資源)
    static int i=0;

    /**
     * synchronized 修飾例項方法
     */
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        AccountingSync instance=new AccountingSync();
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}
複製程式碼

上述程式碼中,我們開啟兩個執行緒操作同一個共享資源即變數i,由於i++;操作並不具備原子性,該操作是先讀取值,然後寫回一個新值,相當於原來的值加上1,分兩步完成,如果第二個執行緒在第一個執行緒讀取舊值和寫回新值期間讀取i的域值,那麼第二個執行緒就會與第一個執行緒一起看到同一個值,並執行相同值的加1操作,這也就造成了執行緒安全失敗,因此對於increase方法必須使用synchronized修飾,以便保證執行緒安全。此時我們應該注意到synchronized修飾的是例項方法increase,在這樣的情況下,當前執行緒的鎖便是例項物件instance,注意Java中的執行緒同步鎖可以是任意物件。從程式碼執行結果來看確實是正確的,倘若我們沒有使用synchronized關鍵字,其最終輸出結果就很可能小於2000000,這便是synchronized關鍵字的作用。

修飾靜態方法:作用於當前類物件(Class物件,每個類都有一個Class物件),進入同步程式碼前要獲得當前類物件(Class物件)的鎖

public class AccountingSyncClass implements Runnable{
    static int i=0;

    /**
     * 作用於靜態方法,鎖是當前class物件,也就是
     * AccountingSyncClass類對應的class物件
     */
    public static synchronized void increase(){
        i++;
    }

    /**
     * 非靜態,訪問時鎖不一樣不會發生互斥
     */
    public synchronized void increase4Obj(){
        i++;
    }

    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新例項
        Thread t1=new Thread(new AccountingSyncClass());
        //new新例項
        Thread t2=new Thread(new AccountingSyncClass());
        //啟動執行緒
        t1.start();t2.start();

        t1.join();t2.join();
        System.out.println(i);
    }
}
複製程式碼

由於synchronized關鍵字修飾的是靜態increase方法,與修飾例項方法不同的是,其鎖物件是當前類的class物件。注意程式碼中的increase4Obj方法是例項方法,其物件鎖是當前例項物件,如果別的執行緒呼叫該方法,將不會產生互斥現象,畢竟鎖物件不同,但我們應該意識到這種情況下可能會發現執行緒安全問題(操作了共享靜態變數i)。

修飾程式碼塊:指定加鎖物件,對給定物件加鎖,進入同步程式碼庫前要獲得給定物件的鎖。

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    @Override
    public void run() {
        //省略其他耗時操作....
        //使用同步程式碼塊對變數i進行同步操作,鎖物件為instance
        synchronized(instance){
            for(int j=0;j<1000000;j++){
                    i++;
              }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}
複製程式碼

從程式碼看出,將synchronized作用於一個給定的括號裡的例項物件instance,即當前例項物件就是鎖物件,每次當執行緒進入synchronized包裹的程式碼塊時就會要求當前執行緒持有instance例項物件鎖,如果當前有其他執行緒正持有該物件鎖,那麼新到的執行緒就必須等待,這樣也就保證了每次只有一個執行緒執行i++;操作。當然除了instance作為物件外,我們還可以使用this物件(代表當前例項)或者當前類的class物件作為鎖,如下程式碼:

//this,當前例項物件鎖
synchronized(this){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

//class物件鎖
synchronized(AccountingSync.class){
    for(int j=0;j<1000000;j++){
        i++;
    }
}
複製程式碼

以上就是java中synchronized關鍵字的用法,很簡單,接下來我們先介紹一些基礎知識,然後一步一步說明synchronize關鍵字的低層實現原理。

2.Java物件頭

HotSpot虛擬機器中,物件在記憶體中儲存的佈局可以分為三塊區域:物件頭(Header)、例項資料(Instance Data)和對齊填充(Padding)。
普通物件的物件頭包括兩部分:Mark Word 和 Class Metadata Address (型別指標),如果是陣列物件還包括一個額外的Array length陣列長度部分。

Mark Word:用於儲存物件自身的執行時資料,如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等等,佔用記憶體大小與虛擬機器位長一致。

Class Metadata Address:型別指標指向物件的類後設資料,虛擬機器通過這個指標確定該物件是哪個類的例項。

Array length:陣列長度

如果物件是陣列型別,則虛擬機器用3個Word(字寬)儲存物件頭,如果物件是非陣列型別,則用2字寬儲存物件頭。在32位虛擬機器中,一字寬等於四位元組,即32bit。

長度 內容 說明
32/64bit Mark Word 儲存物件hashCode或鎖資訊等執行時資料。
32/64bit Class Metadata Address 儲存到物件型別資料的指標
32/64bit Array length 陣列的長度(如果當前物件是陣列)

物件需要儲存的執行時資料很多,其實已經超出了32、64位Bitmap結構所能記錄的限度,但是物件頭資訊是與物件自身定義的資料無關的額外儲存成本,考慮到虛擬機器的空間效率,Mark Word被設計成一個非固定的資料結構以便在極小的空間記憶體儲儘量多的資訊,它會根據物件的狀態複用自己的儲存空間。例如在32位的HotSpot虛擬機器 中物件未被鎖定的狀態下,MarkWord的32個Bits空間中的25Bits用於儲存物件雜湊碼(HashCode),4Bits用於儲存物件分代年齡,2Bits用於儲存鎖標誌位,1Bit固定為0,在其他狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下物件的儲存內容如下表所示。

Synchronized原理

此處可能存在疑問,無鎖狀態時,Mark Word中會儲存hashCode等資訊,在有鎖狀態時,位置被鎖指標佔用,那hashCode等資訊要存到哪裡?是沒有了嗎?這個問題在後面monitor先關的小節會解答。

2.Monitor物件

什麼是Monitor?我們可以把它理解為一個同步工具,也可以描述為一種同步機制,它通常被描述為一個物件。與一切皆物件一樣,所有的Java物件是天生的Monitor,每一個Java物件都有成為Monitor的潛質,因為在Java的設計中,每一個Java物件自打孃胎裡出來就帶了一把看不見的鎖,它叫做內部鎖或者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(鎖)。如下圖所示

Synchronized原理

3.Synchronized原理

3.1 同步程式碼塊

public class SyncCodeBlock {
    public int i;

    public void syncTask(){
        synchronized (this){
            i++;
        }
    }
}
複製程式碼

編譯上述程式碼並使用javap反編譯後得到位元組碼如下(這裡我們省略一部分沒有必要的資訊):

public class com.fufu.concurrent.SyncCodeBlock {
  public int i;

  public com.fufu.concurrent.SyncCodeBlock();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void syncTask();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter                   //注意此處,進入同步方法
       4: aload_0
       5: dup
       6: getfield      #2                  // Field i:I
       9: iconst_1
      10: iadd
      11: putfield      #2                  // Field i:I
      14: aload_1
      15: monitorexit                    //注意此處,退出同步方法
      16: goto          24
      19: astore_2
      20: aload_1
      21: monitorexit                    //注意此處,退出同步方法
      22: aload_2
      23: athrow
      24: return
    Exception table:
       from    to  target type
           4    16    19   any
          19    22    19   any
}

複製程式碼

從位元組碼中可知同步語句塊的實現使用的是monitorenter和monitorexit指令,其中monitorenter指令指向同步程式碼塊的開始位置,monitorexit指令則指明同步程式碼塊的結束位置,當執行monitorenter指令時,當前執行緒將試圖獲取objectref(即物件鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor的進入計數器為 0,那執行緒可以成功取得monitor,並將計數器值設定為1,取鎖成功。

如果當前執行緒已經擁有 objectref 的 monitor 的持有權,那它可以重入這個 monitor (關於重入性稍後會分析),重入時計數器的值也會加 1。倘若其他執行緒已經擁有 objectref 的 monitor的所有權,那當前執行緒將被阻塞,直到正在執行執行緒執行完畢,即monitorexit指令被執行,執行執行緒將釋放 monitor(鎖)並設定計數器值為0 ,其他執行緒將有機會持有 monitor 。

值得注意的是編譯器將會確保無論方法通過何種方式完成,方法中呼叫過的每條 monitorenter 指令都有執行其對應 monitorexit 指令,而無論這個方法是正常結束還是異常結束。為了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器宣告可處理所有的異常,它的目的就是用來執行 monitorexit 指令。從位元組碼中也可以看出多了一個monitorexit指令,它就是異常結束時被執行的釋放monitor 的指令。

3.2 同步方法

public class SyncMethod {

   public int i;

   public synchronized void syncTask(){
           i++;
   }
}
複製程式碼

方法級的同步是隱式,即無需通過位元組碼指令來控制的,它實現在方法呼叫和返回操作之中。JVM可以從方法常量池中的方法表結構(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標誌區分一個方法是否同步方法。當方法呼叫時,呼叫指令將會 檢查方法的 ACC_SYNCHRONIZED訪問標誌是否被設定,如果設定了,執行執行緒將先持有monitor(虛擬機器規範中用的是管程一詞),然後再執行方法,最後再方法完成(無論是正常完成還是非正常完成)時釋放monitor。在方法執行期間,執行執行緒持有了monitor,其他任何執行緒都無法再獲得同一個monitor。如果一個同步方法執行期間丟擲了異常,並且在方法內部無法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法之外時自動釋放。下面我們看看位元組碼層面如何實現:

  //省略沒必要的位元組碼
  //==================syncTask方法======================
  public synchronized void syncTask();
    descriptor: ()V
    //方法標識ACC_PUBLIC代表public修飾,ACC_SYNCHRONIZED指明該方法為同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
複製程式碼

從位元組碼中可以看出,synchronized修飾的方法並沒有monitorenter指令和monitorexit指令,取得代之的確實是ACC_SYNCHRONIZED標識,該標識指明瞭該方法是一個同步方法,JVM通過該ACC_SYNCHRONIZED訪問標誌來辨別一個方法是否宣告為同步方法,從而執行相應的同步呼叫。這便是synchronized鎖在同步程式碼塊和同步方法上實現的基本原理。

4. 鎖優化

上一節看出,Synchronized的實現依賴於與某個物件向關聯的monitor(監視器)實現,而monitor是基於底層作業系統的Mutex Lock實現的,而基於Mutex Lock實現的同步必須經歷從使用者態到核心態的轉換,這個開銷特別大,成本非常高。所以頻繁的通過Synchronized實現同步會嚴重影響到程式效率,而這種依賴於Mutex Lock實現的鎖機制也被稱為“重量級鎖”,為了減少重量級鎖帶來的效能開銷,JDK對Synchronized進行了種種優化。

Java SE1.6為了減少獲得鎖和釋放鎖所帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java SE1.6裡鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。

在看下面內容之前,如果不熟悉CAS是什麼的話,強烈建議看一下這篇關於CAS機制的部落格,java的鎖優化基本上就是基於CAS,對於理解下面內容有很大幫助。《深入淺出CAS》

4.1 偏向鎖

Hotspot的作者經過以往的研究發現大多數情況下鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖。

Synchronized原理
獲取鎖

  1. 檢測Mark Word是否為可偏向狀態,即是否為偏向鎖1,鎖標識位為01;
  2. 若為可偏向狀態,則測試執行緒ID是否為當前執行緒ID,如果是,則執行步驟(5),否則執行步驟(3);
  3. 如果執行緒ID不為當前執行緒ID,則通過CAS操作競爭鎖,競爭成功,則將Mark Word的執行緒ID替換為當前執行緒ID,否則執行執行緒(4);
  4. 通過CAS競爭鎖失敗,證明當前存在多執行緒競爭情況,當到達全域性安全點,獲得偏向鎖的執行緒被掛起,偏向鎖升級為輕量級鎖,然後被阻塞在安全點的執行緒繼續往下執行同步程式碼塊;
  5. 執行同步程式碼塊

釋放鎖: 偏向鎖的釋放採用了一種只有競爭才會釋放鎖的機制,執行緒是不會主動去釋放偏向鎖,需要等待其他執行緒來競爭。偏向鎖的撤銷需要等待全域性安全點(這個時間點是上沒有正在執行的程式碼)。其步驟如下:

  1. 暫停擁有偏向鎖的執行緒,判斷鎖物件石是否還處於被鎖定狀態;
  2. 撤銷偏向蘇,恢復到無鎖狀態(01)或者輕量級鎖的狀態;

此時,解答一下前面的小節中提出的問題,在有鎖狀態時,位置被鎖指標佔用,那hashCode等資訊要存到哪裡?是沒有了嗎?經過在網上苦苦搜尋,終於找到了大神關於次問題的恢復,下面先看偏向鎖的情況,偏向鎖時,mark word中記錄了執行緒id,沒有足夠的額外空間儲存hashcode,所以,答案是:

  1. 當一個物件已經計算過identity hash code,它就無法進入偏向鎖狀態;
  2. 當一個物件當前正處於偏向鎖狀態,並且需要計算其identity hash code的話,則它的偏向鎖會被撤銷,並且鎖會膨脹為重量鎖;
  3. 重量鎖的實現中,ObjectMonitor類裡有欄位可以記錄非加鎖狀態下的mark word,其中可以儲存identity hash code的值。或者簡單說就是重量鎖可以存下identity hash code。

請一定要注意,這裡討論的hash code都只針對identity hash code。使用者自定義的hashCode()方法所返回的值跟這裡討論的不是一回事。Identity hash code是未被覆寫的 java.lang.Object.hashCode() 或者 java.lang.System.identityHashCode(Object) 所返回的值。

因為mark word裡沒地方同時放bias資訊和identity hash code。 HotSpot VM是假定“實際上只有很少物件會計算identity hash code”來做優化的;換句話說如果實際上有很多物件都計算了identity hash code的話,HotSpot VM會被迫使用比較不優化的模式。

作者:RednaxelaFX 連結:www.zhihu.com/question/52… 來源:知乎

4.2 輕量級鎖

引入輕量級鎖的主要目的是在多沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用作業系統互斥量產生的效能消耗。 當關閉偏向鎖功能或者多個執行緒競爭偏向鎖導致偏向鎖升級為輕量級鎖,則會嘗試獲取輕量級鎖,其步驟如下:

Synchronized原理

獲取鎖

  1. 判斷當前物件是否處於無鎖狀態(hashcode、0、01),若是,則JVM首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced字首,即Displaced Mark Word);否則執行步驟(3);
  2. JVM利用CAS操作嘗試將物件的Mark Word更新為指向Lock Record的指正,如果成功表示競爭到鎖,則將鎖標誌位變成00(表示此物件處於輕量級鎖狀態),執行同步操作;如果失敗則執行步驟(3);
  3. 判斷當前物件的Mark Word是否指向當前執行緒的棧幀,如果是則表示當前執行緒已經持有當前物件的鎖,則直接執行同步程式碼塊;否則只能說明該鎖物件已經被其他執行緒搶佔了,這時輕量級鎖需要膨脹為重量級鎖,鎖標誌位變成10,後面等待的執行緒將會進入阻塞狀態;

釋放鎖: 輕量級鎖的釋放也是通過CAS操作來進行的,主要步驟如下:

  1. 取出在獲取輕量級鎖儲存在Displaced Mark Word中的資料;
  2. 用CAS操作將取出的資料替換當前物件的Mark Word中,如果成功,則說明釋放鎖成功,否則執行(3);
  3. 如果CAS操作替換失敗,說明有其他執行緒嘗試獲取該鎖,則需要在釋放鎖的同時需要喚醒被掛起的執行緒。

輕量級鎖狀態時,位置被鎖指標佔用,那hashCode等資訊要存到哪裡?這裡的問題就比較簡單了,因為有拷貝的mark word,所以Displaced Mark Word中存在所需要的資訊。

4.3 重量級鎖

重量級鎖通過物件內部的監視器(monitor)實現,其中monitor的本質是依賴於底層作業系統的Mutex Lock實現,作業系統實現執行緒之間的切換需要從使用者態到核心態的切換,切換成本非常高。

4.4 自旋鎖

輕量級鎖失敗後,虛擬機器為了避免執行緒真實地在作業系統層面掛起,還會進行一項稱為自旋鎖的優化手段。這是基於在大多數情況下,執行緒持有鎖的時間都不會太長,如果直接掛起作業系統層面的執行緒可能會得不償失,畢竟作業系統實現執行緒之間的切換時需要從使用者態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,因此自旋鎖會假設在不久將來,當前的執行緒可以獲得鎖,因此虛擬機器會讓當前想要獲取鎖的執行緒做幾個空迴圈(這也是稱為自旋的原因),一般不會太久,可能是50個迴圈或100迴圈,在經過若干次迴圈後,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會將執行緒在作業系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是可以提升效率的。最後沒辦法也就只能升級為重量級鎖了。自旋是把雙刃劍,如果旋的時間過長會影響整體效能,時間過短又達不到延遲阻塞的目的。顯然,自旋的週期選擇顯得非常重要,但這與作業系統、硬體體系、系統的負載等諸多場景相關,很難選擇,如果選擇不當,不但效能得不到提高,可能還會下降。

4.5 適應自旋鎖

JDK 1.6引入了更加聰明的自旋鎖,即自適應自旋鎖。所謂自適應就意味著自旋的次數不再是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。它怎麼做呢?執行緒如果自旋成功了,那麼下次自旋的次數會更加多,因為虛擬機器認為既然上次成功了,那麼此次自旋也很有可能會再次成功,那麼它就會允許自旋等待持續的次數更多。反之,如果對於某個鎖,很少有自旋能夠成功的,那麼在以後要或者這個鎖的時候自旋的次數會減少甚至省略掉自旋過程,以免浪費處理器資源。

有了自適應自旋鎖,隨著程式執行和效能監控資訊的不斷完善,虛擬機器對程式鎖的狀況預測會越來越準確,虛擬機器會變得越來越聰明。

4.6 鎖消除

為了保證資料的完整性,我們在進行操作時需要對這部分操作進行同步控制,但是在有些情況下,JVM檢測到不可能存在共享資料競爭,這是JVM會對這些同步鎖進行鎖消除。鎖消除的依據是逃逸分析的資料支援。 如果不存在競爭,為什麼還需要加鎖呢?所以鎖消除可以節省毫無意義的請求鎖的時間。變數是否逃逸,對於虛擬機器來說需要使用資料流分析來確定,但是對於我們程式設計師來說這還不清楚麼?我們會在明明知道不存在資料競爭的程式碼塊前加上同步嗎?但是有時候程式並不是我們所想的那樣?我們雖然沒有顯示使用鎖,但是我們在使用一些JDK的內建API時,如StringBuffer、Vector、HashTable等,這個時候會存在隱形的加鎖操作。比如StringBuffer的append()方法,Vector的add()方法。

4.7 鎖的膨脹流程

在前面偏向鎖和輕量級鎖的小節中已經大概瞭解的鎖的膨脹流程:

偏向鎖->輕量級鎖->重量級鎖

偏向所鎖,輕量級鎖都是樂觀鎖,重量級鎖是悲觀鎖。

一個物件剛開始例項化的時候,沒有任何執行緒來訪問它的時候。它是可偏向的,意味著,它現在認為只可能有一個執行緒來訪問它,所以當第一個執行緒來訪問它的時候,它會偏向這個執行緒,此時,物件持有偏向鎖。

偏向第一個執行緒,這個執行緒在修改物件頭成為偏向鎖的時候使用CAS操作,並將物件頭中的ThreadID改成自己的ID,之後再次訪問這個物件時,只需要對比ID,不需要再使用CAS在進行操作。

一旦有第二個執行緒訪問這個物件,因為偏向鎖不會主動釋放,所以第二個執行緒可以看到物件是偏向狀態,這時表明在這個物件上已經存在競爭了,檢查原來持有該物件鎖的執行緒是否依然存活,如果掛了,則可以將物件變為無鎖狀態,然後重新偏向新的執行緒,如果原來的執行緒依然存活,則馬上執行那個執行緒的操作棧,檢查該物件的使用情況,如果仍然需要持有偏向鎖,則偏向鎖升級為輕量級鎖,(偏向鎖就是這個時候升級為輕量級鎖的)。如果不存在使用了,則可以將物件回覆成無鎖狀態,然後重新偏向。

輕量級鎖認為競爭存在,但是競爭的程度很輕,一般兩個執行緒對於同一個鎖的操作都會錯開,或者說稍微等待一下(自旋),另一個執行緒就會釋放鎖。 但是當自旋超過一定的次數,或者一個執行緒在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹為重量級鎖,重量級鎖使除了擁有鎖的執行緒以外的執行緒都阻塞,防止CPU空轉。

下面這張圖,很好的說明了鎖的膨脹流程。

Synchronized原理

相關文章