上幾次部落格,我們把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 | 對象分代年齡 | 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萬個對象。也就是說開啟了逃逸分析有一部分對象隻是建立了線程棧上,當線程棧結束,對象也被銷毀,上面的問題也就有答案了,執行個體對象可能存在堆上,也可能存在棧上。