天天看點

談一談Synchronized底層原理Synchronized底層原理

Synchronized底層原理

sync鎖的是什麼?它是怎麼就把對象給鎖上了?

是不是也應該有個變量來控制呢?

上鎖就是改變對象的對象頭

java對象頭是實作synchronized的鎖對象的基礎,synchronized使用的鎖對象是存儲在Java對象頭裡的。

那麼問題來了。對象頭又是什麼鬼?

什麼是對象頭?

這就要知道java的對象布局,換句話說就是java對象由什麼組成。

在JVM中,對象在記憶體中的布局分為三塊區域:對象頭、執行個體資料和對齊填充。

1、對象頭 ------(大小固定)

2、執行個體資料 ------- (大小不固定)

3、對齊填充

談一談Synchronized底層原理Synchronized底層原理
  • 執行個體變量:存放類的屬性資料資訊,包括父類的屬性資訊,如果是數組的執行個體部分還包括數組的長度,這部分記憶體按4位元組對齊。
  • 填充資料:對齊填充并不是必然存在的,也沒有特别的含義,它僅僅起着占位符的作用。由于HotSpot VM的自動記憶體管理系統要求對象起始位址必須是8位元組的整數倍,換句話說就是對象的大小必須是8位元組的整數倍。對象頭正好是8位元組的倍數(1倍或者2倍),是以當對象執行個體資料部分沒有對齊的話,就需要通過對齊填充來補全。
  • 對象頭:包括運作時中繼資料(Mark Word)和 類型指針(Class Metadata Address)
    鎖狀态 25bit 4bit 1bit是否是偏向鎖 2bit 鎖标志位
    無鎖狀态 對象HashCode GC分代年齡 01
    • 運作時中繼資料
      • 哈希值( HashCode )
      • GC分代年齡
      • 鎖狀态标志
      • 線程持有的鎖
      • 偏向線程ID
      • 偏向時間戳
    • 類型指針:指向類中繼資料的InstanceClass,确定該對象所屬的類型

      ​ 說明:如果是數組,還需記錄數組的長度

談一談Synchronized底層原理Synchronized底層原理

其中輕量級鎖和偏向鎖是Java 6 對 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(鎖)。

如下圖所示:

談一談Synchronized底層原理Synchronized底層原理

由此看來,monitor對象存在于每個Java對象的對象頭中(存儲的指針的指向),synchronized鎖便是通過這種方式擷取鎖的,也是為什麼Java中任意對象可以作為鎖的原因,同時也是notify/notifyAll/wait等方法存在于頂級對象Object中的原因(關于這點稍後還會進行分析),ok~,有了上述知識基礎後,下面我們将進一步分析synchronized在位元組碼層面的具體語義實作。

Synchronized标記代碼塊底層原理

public class SyncCodeBlock {
   public int i;
   public void syncTask(){
       //同步代碼庫
       synchronized (this){
           i++;
       }
   }
}
           

編譯後的位元組碼:

3: monitorenter  //進入同步方法
//..........省略其他  
15: monitorexit   //退出同步方法
16: goto          24
//省略其他.......
21: monitorexit //退出同步方法
           

從位元組碼中可知同步語句塊的實作使用的是

monitorenter

monitorexit

指令

其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結束位置.

擷取鎖的過程:

當執行monitorenter指令時,目前線程将試圖擷取 objectref(即對象鎖) 所對應的 monitor 的持有權。

當 objectref 的 monitor 的計數器為 0,那線程可以成功取得 monitor,将monitor對象的owner變量設定為目前線程,并将計數器值設定為 1,取鎖成功。

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

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

synchronized方法底層原理

方法級的同步是隐式,即無需通過位元組碼指令來控制的,它實作在方法調用和傳回操作之中。JVM可以從方法常量池中的方法表結構(method_info Structure) 中的**

ACC_SYNCHRONIZED

通路标志區分一個方法是否同步方法**。當方法調用時,調用指令将會 檢查方法的

ACC_SYNCHRONIZED

通路标志是否被設定,如果設定了,執行線程将先持有monitor對象, 然後再執行方法,最後再方法完成(無論是正常完成還是非正常完成)時釋放monitor。在方法執行期間,執行線程持有了monitor,其他任何線程都無法再獲得同一個monitor。如果一個同步方法執行期間抛 出了異常,并且在方法内部無法處理此異常,那這個同步方法所持有的monitor将在異常抛到同步方法之外時自動釋放。下面我們看看位元組碼層面如何實作:

public class SyncMethod {
   public int i;
   public synchronized void syncTask(){
           i++;
   }
}
           

編譯後的位元組碼:

//省略沒必要的位元組碼
  //==================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
}
SourceFile: "SyncMethod.java"
           

從位元組碼中可以看出,synchronized修飾的方法并沒有monitorenter指令和monitorexit指令,取得代之的确實是ACC_SYNCHRONIZED辨別.

該辨別指明了該方法是一個同步方法,JVM通過該ACC_SYNCHRONIZED通路标志來辨識一個方法是否聲明為同步方法,進而執行相應的同步調用。

這便是synchronized鎖在同步代碼塊和同步方法上實作的基本原理。同時我們還必須注意到的是在Java早期版本中,synchronized屬于重量級鎖,效率低下,因為螢幕鎖(monitor)是依賴于底層的作業系統的

Mutex Lock

來實作的,而作業系統實作線程之間的切換時需要從使用者态轉換到核心态,這個狀态之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什麼早期的synchronized效率低的原因。慶幸的是在Java 6之後Java官方對從JVM層面對synchronized較大優化,是以現在的synchronized鎖效率也優化得很不錯了,Java 6之後,為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了輕量級鎖和偏向鎖,接下來我們将簡單了解一下Java官方在JVM層面對synchronized鎖的優化。

Java虛拟機對synchronized的優化

鎖的狀态總共有四種,無鎖狀态、偏向鎖、輕量級鎖和重量級鎖。随着鎖的競争,鎖可以從偏向鎖更新到輕量級鎖,再更新的重量級鎖,但是鎖的更新是單向的,也就是說隻能從低到高更新,不會出現鎖的降級,關于重量級鎖,前面我們已詳細分析過,下面我們将介紹偏向鎖和輕量級鎖以及JVM的其他優化手段,這裡并不打算深入到每個鎖的實作和轉換過程更多地是闡述Java虛拟機所提供的每個鎖的核心優化思想,畢竟涉及到具體過程比較繁瑣,如需了解詳細過程可以查閱《深入了解Java虛拟機原理》。

更新過程: 偏向鎖 → 輕量級鎖 →(自旋鎖 )→ 重量級鎖

偏向鎖

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

偏向鎖的核心思想是,如果一個線程獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變為偏向鎖結構,當這個線程再次請求鎖時,無需再做任何同步操作,即擷取鎖的過程,這樣就省去了大量有關鎖申請的操作,進而也就提供程式的性能。

是以,對于沒有鎖競争的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個線程申請相同的鎖。但是對于鎖競争比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的線程都是不相同的,是以這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗後,并不會立即膨脹為重量級鎖,而是先更新為輕量級鎖。下面我們接着了解輕量級鎖。

輕量級鎖

當這樣的場合每次申請鎖的線程都是不相同的,偏向鎖就不适用了,就會更新為輕量級鎖。

為啥叫輕量級鎖,因為這是相比于傳統的重量級鎖而言,原來傳統的重量級鎖,使用的是系統互斥量實作的。

他的出現并不是代替重量級鎖,而是在沒有多線程競争的前提下,減少系統互斥量操作産生的性能消耗。

倘若偏向鎖失敗,虛拟機并不會立即更新為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優化手段(1.6之後加入的),此時Mark Word 的結構也變為輕量級鎖的結構。輕量級鎖能夠提升程式性能的依據是“對絕大部分的鎖,在整個同步周期内都不存在競争”,注意這是經驗資料。如果沒有競争,輕量級鎖使用CAS操作避免了使用互斥量的開銷,但如果存在鎖競争,除了互斥量的開銷外,還額外發生了CAS操作,是以在有競争的情況下,輕量級鎖比傳統的重量級鎖更慢。

需要了解的是,輕量級鎖所适應的場景是線程交替執行同步塊的場合,如果存在同一時間通路同一鎖的場合,就會導緻輕量級鎖膨脹為重量級鎖。

輕量級鎖的加鎖過程:
  • 在代碼進入同步塊的時候,如果同步對象鎖狀态為無鎖狀态(鎖标志位為“01”狀态,是否為偏向鎖為“0”),虛拟機首先将在目前線程的棧幀中建立一個名為**鎖記錄(Lock Record)**的空間,用于存儲鎖對象目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word。這時候線程堆棧與對象頭的狀态如圖:
  • 拷貝對象頭中的Mark Word複制到鎖記錄(Lock Record)中;
  • 拷貝成功後,虛拟機将使用CAS操作嘗試将鎖對象的Mark Word更新為指向Lock Record的指針,并将線程棧幀中的Lock Record裡的owner指針指向Object的 Mark Word。
  • 如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖标志位設定為“00”,即表示此對象處于輕量級鎖定狀态。這時候線程堆棧與對象頭的狀态如圖所示。
    談一談Synchronized底層原理Synchronized底層原理
  • 如果這個更新操作失敗了,虛拟機首先會檢查對象的Mark Word是否指向目前線程的棧幀,如果是就說明目前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明多個線程競争鎖,輕量級鎖就要膨脹為重量級鎖,鎖标志的狀态值變為“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀态。

自旋鎖

輕量級鎖失敗後,虛拟機為了避免線程真實地在作業系統層面挂起,還會進行一項稱為自旋鎖的優化手段。這是基于在大多數情況下,線程持有鎖的時間都不會太長,如果直接挂起作業系統層面的線程可能會得不償失,畢竟作業系統實作線程之間的切換時需要從使用者态轉換到核心态,這個狀态之間的轉換需要相對比較長的時間,時間成本相對較高,是以自旋鎖會假設在不久将來,目前的線程可以獲得鎖,是以虛拟機會讓目前想要擷取鎖的線程做幾個空循環(這也是稱為自旋的原因),一般不會太久,可能是50個循環或100循環,在經過若幹次循環後,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會将線程在作業系統層面挂起,這就是自旋鎖的優化方式,這種方式确實也是可以提升效率的。最後沒辦法也就隻能更新為重量級鎖了。

重量級鎖

重量級鎖也就是通常說synchronized的對象鎖,鎖辨別位為10,其中指針指向的是monitor對象(也稱為管程或螢幕鎖)的起始位址。每個對象都存在着一個 monitor 與之關聯,對象與其 monitor 之間的關系有存在多種實作方式,如monitor可以與對象一起建立銷毀或當線程試圖擷取對象鎖時自動生成,但當一個 monitor 被某個線程持有後,它便處于鎖定狀态。

鎖消除

消除鎖是虛拟機另外一種鎖的優化,這種優化更徹底,Java虛拟機在JIT編譯時(可以簡單了解為當某段代碼即将第一次被執行時進行編譯,又稱即時編譯),通過對運作上下文的掃描,去除不可能存在共享資源競争的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間,如下StringBuffer的append是一個同步方法,但是在add方法中的StringBuffer屬于一個局部變量,并且不會被其他線程所使用,是以StringBuffer不可能存在共享資源競争的情景,JVM會自動将其鎖消除。

synchronized的可重入性

從互斥鎖的設計上來說,當一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時,将會處于阻塞狀态,但當一個線程再次請求自己持有對象鎖的臨界資源時,這種情況屬于重入鎖,請求将會成功,在java中synchronized是基于原子性的内部鎖機制,是可重入的,是以在一個線程調用synchronized方法的同時在其方法體内部調用該對象另一個synchronized方法,也就是說一個線程得到一個對象鎖後再次請求該對象鎖,是允許的,這就是synchronized的可重入性。如下:

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    static int j=0;
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){

            //this,目前執行個體對象鎖
            synchronized(this){
                i++;
                increase();//synchronized的可重入性
            }
        }
    }
    public synchronized void increase(){
        j++;
    }

    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代碼塊執行同步代碼,并在代碼塊中調用了目前執行個體對象的另外一個synchronized方法,再次請求目前執行個體鎖時,将被允許,進而執行方法體代碼,這就是重入鎖最直接的展現,需要特别注意另外一種情況,當子類繼承父類時,子類也是可以通過可重入鎖調用父類的同步方法。注意由于synchronized是基于monitor實作的,是以每次重入,monitor中的計數器仍會加1。

等待喚醒機制與synchronized

所謂等待喚醒機制本篇主要指的是notify/notifyAll和wait方法,在使用這3個方法時,必須處于synchronized代碼塊或者synchronized方法中,否則就會抛出IllegalMonitorStateException異常,這是因為調用這幾個方法前必須拿到目前對象的螢幕monitor對象,也就是說notify/notifyAll和wait方法依賴于monitor對象,在前面的分析中,我們知道monitor 存在于對象頭的Mark Word 中(存儲monitor引用指針),而synchronized關鍵字可以擷取 monitor ,這也就是為什麼notify/notifyAll和wait方法必須在synchronized代碼塊或者synchronized方法調用的原因。