synchronized 被稱為對象鎖。
常見三種使用方法:
1)普通同步方法,鎖是目前執行個體;
2)靜态同步方法,鎖是目前類的Class執行個體,Class資料存在永久代中,是該類的一個全局鎖;
3)對于同步代碼塊,鎖是synchronized括号裡配置的對象。
public class SynchronizedBlock {
public void method() {
//上鎖
synchronized (this) {
System.out.println("...");
}
System.out.println("...");
}
}
反彙編後的代碼(javap -c)
public void method();
0 aload_0 [this]
1 dup
2 astore_1
3 monitorenter //在同步塊開始位置插入monitorenter指令
4 getstatic java.lang.System.out : java.io.PrintStream [15]
7 ldc <String "..."> [21]
9 invokevirtual java.io.PrintStream.println(java.lang.String) : void [23]
12 aload_1
13 monitorexit //在同步塊結束位置插入
14 goto 20
17 aload_1
18 monitorexit //在抛出異常位置釋放鎖
19 athrow //抛出異常指令
20 getstatic java.lang.System.out : java.io.PrintStream [15]
23 ldc <String "..."> [21]
25 invokevirtual java.io.PrintStream.println(java.lang.String) : void [23]
28 return
通過反彙編代碼可以觀察到:
同步代碼塊是使用MonitorEnter和MoniterExit指令實作的,在編譯時,MonitorEnter指令被插入到同步代碼塊的開始位置,MoniterExit指令被插入到同步代碼塊的結束位置和異常位置。任何對象都有一個Monitor與之關聯,當Monitor被持有後将處于鎖定狀态。MonitorEnter指令會嘗試擷取Monitor的持有權,即嘗試擷取鎖。
同步方法依賴flags标志ACC_SYNCHRONIZED實作,位元組碼中沒有具體的邏輯,可能需要檢視JVM的底層實作(同步方法也可以通過Monitor指令實作)。ACC_SYNCHRONIZED标志表示方法為同步方法,如果為非靜态方法(沒有ACC_STATIC标志),使用調用該方法的對象作為鎖對象;如果為靜态方法(有ACC_STATIC标志),使用該方法所屬的Class類在JVM的内部對象表示Klass作為鎖對象。
下面是摘自《Java虛拟機規範》的話:
Java虛拟機可以支援方法級的同步和方法内部一段指令序列的同步,這兩種同步結構都是使用管程(Monitor)來支援的。
方法級的同步是隐式,即無需通過位元組碼指令來控制的,它實作在方法調用和傳回操作之中。虛拟機可以從方法常量池中的方法表結構中的ACC_SYNCHRONIZED通路标志區分一個方法是否同步方法。當方法調用時,調用指令将會檢查方法的ACC_SYNCHRONIZED通路标志是否被設定,如果設定了,執行線程将先持有管程,然後再執行方法,最後在方法完成(無論是正常完成還是非正常完成)時釋放管程。在方法執行期間,執行線程持有了管程,其他任何線程都無法再獲得同一個管程。如果一個同步方法執行期間抛出了異常,并且在方法内部無法處理此異常,那這個同步方法所持有的管程将在異常抛到同步方法之外時自動釋放。
同步一段指令集序列通常是由Java語言中的synchronized塊來表示的,Java虛拟機的指令集中有monitorenter和monitorexit兩條指令來支援synchronized關鍵字的語義,正确實作synchronized關鍵字需要編譯器與Java虛拟機兩者協作支援。
Java虛拟機中的同步(Synchronization)基于進入和退出管程(Monitor)對象實作。無論是顯式同步(有明确的monitorenter和monitorexit指令)還是隐式同步(依賴方法調用和傳回指令實作的)都是如此。
編譯器必須確定無論方法通過何種方式完成,方法中調用過的每條monitorenter指令都必須有執行其對應monitorexit指令,而無論這個方法是正常結束還是異常結束。為了保證在方法異常完成時monitorenter和monitorexit指令依然可以正确配對執行,編譯器會自動産生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執行monitorexit指令。
Java對象頭
對象頭含有三部分:Mark Word(存儲對象自身運作時資料)、Class Metadata Address(存儲類中繼資料的指針)、Array length(數組長度,隻有數組類型才有)。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICM38FdsYkRGZkRG9lcvx2bjxiNx8VZ6l2cs0TPnNGbaNjY6t2MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLzcTM3AjN0MTMzEzMwkTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
重點在Mark Word部分,Mark Word資料結構被設計成非固定的,會随着對象的不同狀态而變化,如下表所示。
鎖狀态 | 25bit | 4bit | 1bit是否是偏向鎖 | 2bit鎖标志位 | |
無鎖狀态 | hashCode | age | 01 | ||
輕量級鎖 | 執行棧中鎖記錄的指針 | 00 | |||
重量級鎖 | 執行棧中鎖記錄的指針 | 10 | |||
GC标記 | 空 | 11 | |||
偏向鎖 | 線程ID | Epoch | age | 1 | 01 |
Monitor
Monitor可以了解為一種同步工具,也可了解為一種同步機制,常常被描述為一個Java對象。當使用monitorenter和monitorexit指令的時候,就會進入它的一種模式,而被synchronized對像頭的Mark word就相當于參數。
圖中所知,鎖的級别從低到高:無鎖、偏向鎖、輕量級鎖、重量級鎖。
優點 | 缺點 | 适用情況 | |
偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執行非同步代碼相差無幾。 | 如果線程存在鎖競争,需要額外的鎖撤銷的消耗。 | 适用于隻有一個線程通路同步塊的情況 |
輕量級鎖 | 競争的線程不會阻塞,提高了響應速度 | 長時間得不到鎖的線程使用自旋消耗CPU | 追求響應速度。同步代碼執行非常快 |
重量級鎖 | 線程競争不會使用自旋,不會消耗CPU | 線程出現競争時會阻塞,響應速度慢 | 追求吞吐量。同步代碼執行時間長 |
- 對象正常情況下是無鎖狀态
- 當線程A請求資源,則用cas的方式設定對象頭,更新為偏向鎖。
- 當線程B請求資源,用cas的方式設定對象頭失敗,則暫停A線程任務更新為輕量鎖,再繼續執行A。線程B以cas的方式繼續請求資源
- 當線程B以cas的方式繼續請求資源次數過多,更新為重量鎖
重量級鎖原理
- 所有請求鎖的被阻塞的先會入到ContentionList中
- 當碰到鎖釋放時,ContentionList會被移入EntryList(排隊),并喚醒EntryList的head節點成為就緒節點(OnDeck)
- Ower線程并不是把鎖傳遞給OnDeck線程,OnDeck線程會和 正在請求鎖的線程(進ContentionList之前)競争鎖資源(不公平)
- Ower線程中調用wait,會進入blocking隊列
- 如果碰到notity/notityAll,blocking隊列會移入EntryList