java架構之路(多線程)synchronized詳解以及鎖的膨脹升級過程

  上幾次博客,我們把volatile基本都說完了,剩下的還有我們的synchronized,還有我們的AQS,這次博客我來說一下synchronized的使用和原理。

  synchronized是jvm內部的一把隱式鎖,一切的加鎖和解鎖過程是由jvm虛擬機來控制的,不需要我們認爲的干預,我們大致從瞭解鎖,到synchronized的使用,到鎖的膨脹升級過程三個角度來說一下synchronized。

鎖的分類

  java中我們聽到很多的鎖,什麼顯示鎖,隱式鎖,公平鎖,重入鎖等等,下面我來總結一張圖來供大家學習使用。

 這次博客我們主要來說我們的隱示鎖,就是我們的無鎖到重量級鎖。

synchronized的使用

  我們先來看一段簡單的代碼

public class SynchronizedTest {

    private static Object object = new Object();
    
    public static void main(String[] args) {
        synchronized (object){
            System.out.println("只有我拿到鎖啦");
        }
    }
}

  就這樣synchronized就可以使用了,這樣是每次去拿全局對象的object去鎖住後續的代碼段。我們來看一下彙編指令碼

 public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field object:Ljava/lang/Object;
       3: dup
       4: astore_1
       5: monitorenter
       6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       9: ldc           #4                  // String 只有我拿到鎖啦
      11: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      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
           6    16    19   any
          19    22    19   any

  明顯看到了兩個很重要的方法monitorenter和monitorexit兩個方法,也就是說我們的synchronized方法加鎖是基於monitorenter加鎖和monitorexit解鎖來操作的

  我們得知是由monitorenter來控制加鎖和monitorexit解鎖的,我們完全可以這樣來操作。上次我們說過一個unsafe類。

public class SynchronizedTest {

    private static Object obj = new Object();

    public void lockMethod(){
        UnsafeInstance.reflectGetUnsafe().monitorEnter(obj);
    }
    
    public void unLockMethod(){
        UnsafeInstance.reflectGetUnsafe().monitorExit(obj);
    }
}

  就是我們上次說的unsafe那個類給我們提供了加鎖和解鎖的方法,這樣就是實現誇方法的加鎖和解鎖了,但是超級不建議這樣的使用,後面的AQS回去說別的方式。越過虛擬機直接操作底層的,我們一般是不建議這樣來做的。

  我們還可以將synchronized鎖放置在方法上。例如

public class SynchronizedTest {

    private static Object object = new Object();

    public static synchronized void lockMethod() {
        System.out.println("只有我拿到鎖啦");
    }
}

  這樣加鎖是加在了this當前類對象上的。如果不加static,鎖是加在類對象上的,需要注意我們用的spring的bean作用域

  並且我們的synchronized是一個可重入鎖,在jvm源碼中有一個數值來記錄加鎖和解鎖的次數,所以我們是可以多次套用synchronized的

public void lockMethod(){
    synchronized(obj){
        synchronized(obj){
            System.out.println("我沒報錯");
        }
    }
}

synchronized到底鎖了什麼

  還是拿上個每次加鎖的時候會在對象頭內記錄我們的加鎖信息,我們這裏來說一下對象頭裏面都放置了什麼吧。

以32位JVM內部存儲結構爲例

鎖狀態

25 bit

4bit

1bit

2bit

鎖標誌位

是否是偏向鎖

23bit

2bit

GC標記

11

重量級鎖

指向重量級鎖Monitor的指針(依賴Mutex操作系統的互斥)

 

10

輕量級鎖

指向線程棧中鎖記錄的指針

pointer to Lock Record

00

偏向鎖

線程ID

Epoch

對象分代年齡

1

01

無鎖

對象的hashCode

對象分代年齡

0

01

  由此看出對象一直是有一個位置來記錄我們的鎖信息的。說到這我們就可以來看一下我們鎖的膨脹升級過程了。

鎖的膨脹升級

  我們說過了對象頭的內容,接下來可以說說我們的鎖內部是如何升級上鎖的了。從無鎖到重量級鎖的一個升級過程,我們來邊畫圖,邊詳細看一下。

  無鎖狀態:

   開始時應該這樣的,線程A和線程B要去爭搶鎖對象,但還未開始爭搶,鎖對象的對象頭是無鎖的狀態也就是25bit位存的hashCode,4bit位存的對象的分代年齡,1bit位記錄是否爲偏向鎖,2bit位記錄狀態,優先看最後2bit位,是01,所以說我們的對象可能無鎖或者偏向鎖狀態的,繼續前移一個位置,有1bit專門記錄是否爲偏向鎖的,1代表是偏向鎖,0代表無鎖,剛剛開始的時候一定是一個無鎖的狀態,這個不需要多做解釋,系統不同內部bit位存的東西可能有略微差異,但關鍵信息是一致的。

  偏向鎖:

  這時線程開始佔有鎖對象,比如線程A得到了鎖對象。 

 就會變成這樣的,線程A拿到鎖對象,將我們的偏向鎖標誌位改爲1,並且將原有的hashCode的位置變爲23bit位存放線程A的線程ID(用CAS算法得到的線程A的ID),2bit位存epoch,偏向鎖是永遠不會被釋放的。

  接下來,線程B也開始運行,線程B也希望得到這把鎖啊,於是線程B會檢查23bit位存的是不是自己的線程ID,因爲被線程A已經持有了,一定鎖的23bit位一定不是線程B的線程ID了

   然後線程B也會不甘示弱啊,會嘗試修改一次23bit位的對象頭存儲,如果說這時恰好線程A釋放了鎖,可以修改成功,然後線程B就可以持有該偏向鎖了。如果修改失敗,開始升級鎖。自己無法修改,線程B只能找“大哥”了,線程B會通知虛擬機撤銷偏向鎖,然後虛擬機會撤銷偏向鎖,並告知線程A到達安全點進行等待。線程A到達了安全點,會再次判斷線程是否已經退出了同步塊,如果退出了,將23bit位置空,這時鎖不需要升級,線程B可以直接進行使用了,還是將23bit的null改爲線程B的線程ID就可以了。

   輕量級鎖:如果線程B沒有拿到鎖,我們就會升級到輕量級鎖,首先會在線程A和線程B都開闢一塊LockRecord空間,然後把鎖對象複製一份到自己的LockRecord空間下,並且開闢一塊owner空間留作執行鎖使用,並且鎖對象的前30bit位合併,等待線程A和線程B來修改指向自己的線程,假如線程A修改成功,則鎖對象頭的前30bit位會存線程A的LockRecord的內存地址,並且線程A的owner也會存一份鎖對象的內存地址,形成一個雙向指向的形式。而線程B修改失敗,則進入一個自旋狀態,就是持續來修改鎖對象。

   重量級鎖:如果說線程B多次自旋以後還是遲遲沒有拿到鎖,他會繼續上告,告知虛擬機,我多次自旋還是沒有拿到鎖,這時我們的線程B會由用戶態切換到內核態,申請一個互斥量,並且將鎖對象的前30bit指向我們的互斥量地址,並且進入睡眠狀態,然後我們的線程A繼續運行知道完成時,當線程A想要釋放鎖資源時,發現原來鎖的前30bit位並不是指向自己了,這時線程A釋放鎖,並且去喚醒那些處於睡眠狀態的線程,鎖升級到重量級鎖。

逃逸分析

  很簡單的一個問題,實例對象存在哪裏?到底是堆還是棧?問題我先不回答,我們先看一段代碼。

public class Test {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("開始");
        for (int i = 0; i < 500000; i++) {
            createCar();
        }
        System.out.println("結束");
        Thread.sleep(10000000);
    }


    private static void createCar() {
        Car car = new Car();
    }
}

就是我們運行一個創建對象的方法,一次性創建50萬個Car對象,然後我們讓我們的線程進行深度的睡眠,兩個打印是爲了知道我們的對象已經開始創建了和已經創建完成了。我們來運行一下。

 然後運行jmap -histo命令來查看我們的線程

   我們可以看到,car對象並沒有產生50萬個,別說會被GC掉對象,在運行之前我已經加了GC日誌的參數-XX:+PrintGCDetails,控制檯沒有打印任何GC日誌的。那麼爲什麼會這樣呢?我們來看一下我們的代碼,由createCar代碼創建了car對象,但car對象並沒有被其它的方法或者線程去調用,虛擬機會認爲你這對象可能只是一個實例化,並沒有進行使用,這時虛擬機會給予你一個優化,就是對於可能沒有使用的對象進行一次逃逸,也就是我們說到的逃逸分析。我們加入 -XX:­DoEscapeAnalysis參數再看一次。

   這也就是關閉了我們的逃逸分析,虛擬機就會真的爲我們創建了50萬個對象。也就是說開啓了逃逸分析有一部分對象只是創建了線程棧上,當線程棧結束,對象也被銷燬,上面的問題也就有答案了,實例對象可能存在堆上,也可能存在棧上。

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