深刻分析synchronized原理和鎖膨脹過程(二)

image

前言

上一篇文章介紹了多線程的概念及synchronized的使用方法《synchronized的使用(一)》,可是僅僅會用仍是不夠的,只有瞭解其底層實現才能在開發過程當中指揮若定,因此本篇探討synchronized的實現原理及鎖升級(膨脹)的過程。html

synchronized實現原理

synchronized是依賴於JVM來實現同步的,在同步方法和代碼塊的原理有點區別。java

同步代碼塊

咱們在代碼塊加上synchronized關鍵字數組

public void synSay() {
    synchronized (object) {
        System.out.println("synSay----" + Thread.currentThread().getName());
    }
}
複製代碼

編譯以後,咱們利用反編譯命令javap -v xxx.class查看對應的字節碼,這裏爲了減小篇幅,我就只粘貼對應的方法的字節碼。安全

public void synSay();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: getfield      #2 // Field object:Ljava/lang/String;
         4: dup
         5: astore_1
         6: monitorenter
         7: getstatic     #3 // Field java/lang/System.out:Ljava/io/PrintStream;
        10: new           #4 // class java/lang/StringBuilder
        13: dup
        14: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
        17: ldc           #6 // String synSay----
        19: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: invokestatic  #8 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
        25: invokevirtual #9 // Method java/lang/Thread.getName:()Ljava/lang/String;
        28: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        31: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        34: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        37: aload_1
        38: monitorexit
        39: goto          47
        42: astore_2
        43: aload_1
        44: monitorexit
        45: aload_2
        46: athrow
        47: return
      Exception table:
         from    to  target type
             7    39    42   any
            42    45    42   any
      LineNumberTable:
        line 21: 0
        line 22: 7
        line 23: 37
        line 24: 47
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      48     0  this   Lcn/T1;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 42
          locals = [ class cn/T1, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
複製代碼

能夠發現synchronized同步代碼塊是經過加monitorentermonitorexit指令實現的。
每一個對象都有個**監視器鎖(monitor) **,當monitor被佔用的時候就表明對象處於鎖定狀態,而monitorenter指令的做用就是獲取monitor的全部權,monitorexit的做用是釋放monitor的全部權,這二者的工做流程以下:
monitorenterbash

  1. 若是monitor的進入數爲0,則線程進入到monitor,而後將進入數設置爲1,該線程稱爲monitor的全部者。
  2. 若是是線程已經擁有此monitor(即monitor進入數不爲0),而後該線程又從新進入monitor,則將monitor的進入數+1,這個即爲鎖的重入
  3. 若是其餘線程已經佔用了monitor,則該線程進入到阻塞狀態,知道monitor的進入數爲0,該線程再去從新嘗試獲取monitor的全部權

monitorexit:執行該指令的線程必須是monitor的全部者,指令執行時,monitor進入數-1,若是-1後進入數爲0,那麼線程退出monitor,再也不是這個monitor的全部者。這個時候其它阻塞的線程能夠嘗試獲取monitor的全部權。多線程

同步方法

在方法上加上synchronized關鍵字併發

synchronized public void synSay() {
    System.out.println("synSay----" + Thread.currentThread().getName());
}
複製代碼

編譯以後,咱們利用反編譯命令javap -v xxx.class查看對應的字節碼,這裏爲了減小篇幅,我就只粘貼對應的方法的字節碼。app

public synchronized void synSay();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #3 // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
        10: ldc           #5 // String synSay----
        12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: invokestatic  #7 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
        18: invokevirtual #8 // Method java/lang/Thread.getName:()Ljava/lang/String;
        21: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        24: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        27: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        30: return
      LineNumberTable:
        line 20: 0
        line 21: 30
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  this   Lcn/T1;
複製代碼

從字節碼上看,加有synchronized關鍵字的方法,常量池中比普通的方法多了個ACC_SYNCHRONIZED標識,JVM就是根據這個標識來實現方法的同步。
當調用方法的時候,調用指令會檢查方法是否有ACC_SYNCHRONIZED標識,有的話線程須要先獲取monitor,獲取成功才能繼續執行方法,方法執行完畢以後,線程再釋放monitor,同一個monitor同一時刻只能被一個線程擁有。佈局

兩種同步方式區別

synchronized同步代碼塊的時候經過加入字節碼monitorentermonitorexit指令來實現monitor的獲取和釋放,也就是須要JVM經過字節碼顯式的去獲取和釋放monitor實現同步,而synchronized同步方法的時候,沒有使用這兩個指令,而是檢查方法的ACC_SYNCHRONIZED標誌是否被設置,若是設置了則線程須要先去獲取monitor,執行完畢了線程再釋放monitor,也就是不須要JVM去顯式的實現。
這兩個同步方式實際都是經過獲取monitor和釋放monitor來實現同步的,而monitor的實現依賴於底層操做系統的mutex互斥原語,而操做系統實現線程之間的切換的時候須要從用戶態轉到內核態,這個轉成過程開銷比較大。
線程獲取、釋放monitor的過程以下:post

線程嘗試獲取monitor的全部權,若是獲取失敗說明monitor被其餘線程佔用,則將線程加入到的同步隊列中,等待其餘線程釋放monitor當其餘線程釋放monitor後,有可能恰好有線程來獲取monitor的全部權,那麼系統會將monitor的全部權給這個線程,而不會去喚醒同步隊列的第一個節點去獲取,因此synchronized是非公平鎖。若是線程獲取monitor成功則進入到monitor中,而且將其進入數+1

關於什麼是公平鎖、非公平鎖能夠參考一下美團技術團隊寫的《不可不說的Java「鎖」事》

到這裏咱們也清楚了synchronized的語義底層是經過一個monitor的對象完成,其實waitnotiyfnotifyAll等方法也是依賴於monitor對象來完成的,這也就是爲何須要在同步方法或者同步代碼塊中調用的緣由(須要先獲取對象的鎖,才能執行),不然會拋出java.lang.IllegalMonitorStateException的異常

Java對象的組成

咱們知道了線程要訪問同步方法、代碼塊的時候,首先須要取得鎖,在退出或者拋出異常的時候又必須釋放鎖,那麼鎖究竟是什麼?又儲存在哪裏?
爲了解開這個疑問,咱們須要進入Java虛擬機(JVM) 的世界。在HotSpot虛擬機中,Java對象在內存中儲存的佈局能夠分爲3塊區域:對象頭實例數據對齊填充synchronized使用的鎖對象儲存在對象頭中

對象頭

對象頭的數據長度在32位和64位(未開啓壓縮指針)的虛擬機中分別爲32bit64bit。對象頭由如下三個部分組成:

  • Mark Word:記錄了對象和鎖的有關信息,儲存對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖標誌位、線程持有的鎖、偏向線程ID、偏向時間戳、對象分代年齡等。注意這個Mark Word結構並非固定的,它會隨着鎖狀態標誌的變化而變化,並且裏面的數據也會隨着鎖狀態標誌的變化而變化,這樣作的目的是爲了節省空間
  • 類型指針:指向對象的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。
  • 數組長度:這個屬性只有數組對象纔有,儲存着數組對象的長度。

32位虛擬機下,Mark Word的結構和數據可能爲如下5種中的一種。

64位虛擬機下,Mark Word的結構和數據可能爲如下2種中的一種。

這裏重點注意是否偏向鎖鎖標誌位,這兩個標識和synchronized的鎖膨脹息息相關。

實例數據

儲存着對象的實際數據,也就是咱們在程序中定義的各類類型的字段內容。

對齊填充

HotSpot虛擬機的對齊方式爲8字節對齊,即一個對象必須爲8字節的整數倍,若是不是,則經過這個對齊填充來佔位填充。

synchronized鎖膨脹過程

上文介紹的 "synchronized實現原理" 實際是synchronized實現重量級鎖的原理,那麼上文頻繁提到monitor對象和對象又存在什麼關係呢,或者說monitor對象儲存在對象的哪一個地方呢?
在對象的對象頭中,當鎖的狀態爲重量級鎖的時候,它的指針即指向monitor對象,如圖:

那鎖的狀態爲其它狀態的時候是否是就沒用上 monitor對象?答案:是的。
這也是 JVMsynchronized的優化,咱們知道重量級鎖的實現是基於底層操做系統的 mutex互斥原語的,這個開銷是很大的。因此 JVMsynchronized作了優化, JVM先利用對象頭實現鎖的功能,若是線程的競爭過大則會將鎖升級(膨脹)爲重量級鎖,也就是使用 monitor對象。固然 JVM對鎖的優化不只僅只有這個,還有引入適應性自旋、鎖消除、鎖粗化、輕量級鎖、偏向鎖等。

那麼鎖的是怎麼進行膨脹的或者依據什麼來膨脹,這也就是本篇須要介紹的重點,首先咱們須要瞭解幾個概念。

鎖的優化

自旋鎖和自適應性自旋鎖

自旋:當有個線程A去請求某個鎖的時候,這個鎖正在被其它線程佔用,可是線程A並不會立刻進入阻塞狀態,而是循環請求鎖(自旋)。這樣作的目的是由於不少時候持有鎖的線程會很快釋放鎖的,線程A能夠嘗試一直請求鎖,不必被掛起放棄CPU時間片,由於線程被掛起而後到喚醒這個過程開銷很大,固然若是線程A自旋指定的時間尚未得到鎖,仍然會被掛起。

自適應性自旋:自適應性自旋是自旋的升級、優化,自旋的時間再也不固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態決定。例如線程若是自旋成功了,那麼下次自旋的次數會增多,由於JVM認爲既然上次成功了,那麼此次自旋也頗有可能成功,那麼它會容許自旋的次數更多。反之,若是對於某個鎖,自旋不多成功,那麼在之後獲取這個鎖的時候,自旋的次數會變少甚至忽略,避免浪費處理器資源。有了自適應性自旋,隨着程序運行和性能監控信息的不斷完善,JVM對程序鎖的情況預測就會變得愈來愈準確,JVM也就變得愈來愈聰明。

鎖消除

鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,可是被檢測到不可能存在共享數據競爭的鎖進行消除

鎖粗化

在使用鎖的時候,須要讓同步塊的做用範圍儘量小,這樣作的目的是爲了使須要同步的操做數量儘量小,若是存在鎖競爭,那麼等待鎖的線程也能儘快拿到鎖

輕量級鎖

所謂輕量級鎖是相對於使用底層操做系統mutex互斥原語實現同步的重量級鎖而言的,由於輕量級鎖同步的實現是基於對象頭的Mark Word。那麼輕量級鎖是怎麼使用對象頭來實現同步的呢,咱們看看具體實現過程。

獲取鎖過程

  1. 在線程進入同步方法、同步塊的時候,若是同步對象鎖狀態爲無鎖狀態(鎖標誌位爲"01"狀態,是否爲偏向鎖爲"0"),虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Recored)的空間,用於儲存鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了個Displaced前綴,即Displaced Mark Word)。

  1. 將對象頭的Mark Word拷貝到線程的鎖記錄(Lock Recored)中。
  2. 拷貝成功後,虛擬機將使用CAS操做嘗試將對象的Mark Word更新爲指向Lock Record的指針。若是這個更新成功了,則執行步驟4,不然執行步驟5
  3. 更新成功,這個線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位將轉變爲"00",即表示此對象處於輕量級鎖的狀態。

  1. 更新失敗,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,若是是就說明當前線程已經擁有了這個對象的鎖,能夠直接進入同步塊繼續執行,不然說明這個鎖對象已經被其其它線程搶佔了。進行自旋執行步驟3,若是自旋結束仍然沒有得到鎖,輕量級鎖就須要膨脹爲重量級鎖,鎖標誌位狀態值變爲"10",Mark Word中儲存就是指向monitor對象的指針,當前線程以及後面等待鎖的線程也要進入阻塞狀態。

釋放鎖的過程

  1. 使用CAS操做將對象當前的Mark Word和線程中複製的Displaced Mark Word替換回來(依據Mark Word中鎖記錄指針是否還指向本線程的鎖記錄),若是替換成功,則執行步驟2,不然執行步驟3
  2. 若是替換成功,整個同步過程就完成了,恢復到無鎖的狀態(01)。
  3. 若是替換失敗,說明有其餘線程嘗試獲取該鎖(此時鎖已膨脹),那就要在釋放鎖的同時,喚醒被掛起的線程。

偏向鎖

偏向鎖的目的是消除數據在無競爭狀況下的同步原語,進一步提升程序的運行性能。若是說輕量級鎖是在無競爭的狀況下使用CAS操做區消除同步使用的互斥量,那麼偏向鎖就是在無競爭的狀況下把整個同步都消除掉,連CAS操做都不用作了。偏向鎖默認是開啓的,也能夠關閉
偏向鎖"偏",就是"偏愛"的"偏",它的意思是這個鎖會偏向於第一個得到它的程序,若是在接下來的執行過程當中,該鎖沒有被其餘的線程獲取,則持有偏向鎖的線程將永遠不須要再進行同步。

獲取鎖的過程

  1. 檢查Mark Word是否爲可偏向鎖的狀態,便是否偏向鎖即爲1即表示支持可偏向鎖,不然爲0表示不支持可偏向鎖。
  2. 若是是可偏向鎖,則檢查Mark Word儲存的線程ID是否爲當前線程ID,若是是則執行同步塊,不然執行步驟3
  3. 若是檢查到Mark WordID不是本線程的ID,則經過CAS操做去修改線程ID修改爲本線程的ID,若是修改爲功則執行同步代碼塊,不然執行步驟4
  4. 當擁有該鎖的線程到達安全點以後,掛起這個線程,升級爲輕量級鎖。

鎖釋放的過程

  1. 有其餘線程來獲取這個鎖,偏向鎖的釋放採用了一種只有競爭纔會釋放鎖的機制,線程是不會主動去釋放偏向鎖,須要等待其餘線程來競爭。
  2. 等待全局安全點(在這個是時間點上沒有字節碼正在執行)。
  3. 暫停擁有偏向鎖的線程,檢查持有偏向鎖的線程是否活着,若是不處於活動狀態,則將對象頭設置爲無鎖狀態,不然設置爲被鎖定狀態。若是鎖對象處於無鎖狀態,則恢復到無鎖狀態(01),以容許其餘線程競爭,若是鎖對象處於鎖定狀態,則掛起持有偏向鎖的線程,並將對象頭Mark Word的鎖記錄指針改爲當前線程的鎖記錄,鎖升級爲輕量級鎖狀態(00)

鎖的轉換過程

鎖主要存在4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭的狀況逐漸升級,這幾個鎖只有重量級鎖是須要使用操做系統底層mutex互斥原語來實現,其餘的鎖都是使用對象頭來實現的。須要注意鎖能夠升級,可是不能夠降級。

這裏盜個圖,這個圖總結的挺好的!(圖被壓縮過了 看不清,能夠打開這個地址查看高清圖 >>高清圖<<)

三種鎖的優缺點比較

image

參考

深刻理解Java虛擬機 Java的對象頭和對象組成詳解
JVM(三)JVM中對象的內存佈局詳解
JVM——深刻分析對象的內存佈局
啃碎併發(七):深刻分析Synchronized原理
Java Synchronized實現原理

原文地址:ddnd.cn/2019/03/22/…

相關文章
相關標籤/搜索