java併發源碼:Synchronized
synchronized加鎖方式:
- 修飾實例方法:鎖是當前對象。
- 修飾靜態方法:鎖是當前類的class對象。
- 修飾代碼塊:鎖是synchronized括號裏配置的對象。
JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步的。代碼塊同步是用monitorenter 和monitorexit指令實現的。
public void test() {
synchronized (this) {
x++;
}
}
Code: 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: aload_0 5: dup 6: getfield #2 // Field x:I 9: iconst_1 10: iadd 11: putfield #2 // Field x:I 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 4 16 19 any 19 22 19 any
可以看到3的monitorenter指令和15的monitorexit指令
synchronized用的鎖是存在java對象頭裏的。32位虛擬機java對象頭的存儲結構:
鎖狀態 | 25bit | 4bit | 1bit是否偏向鎖 | 2bit鎖標誌位 |
---|---|---|---|---|
無鎖狀態 | 對象的hashcode | 對象分代年齡 | 0 | 01 |
在運行期間,MarkWord數據會發生動態變化
鎖狀態 | 25bit | 4bit | 1bit | 2bit | |
---|---|---|---|---|---|
23bit | 2bit | 是否是偏向鎖 | 鎖標誌位 | ||
輕量級鎖 | 00 | ||||
重量級鎖 | 10 | ||||
GC標記 | |||||
線程ID | Epoch | 對象分代年齡 | 1 | 01 |
在64位虛擬機下,MarkWord存儲結構:
鎖狀態 | 25bit | 31bit | 1bit | 4bit | 1bit | 2bit |
---|---|---|---|---|---|---|
cms_free | 分代年齡 | 偏向鎖 | 鎖標誌位 | |||
無鎖 | unused | hashCode | 0 | 01 | ||
偏向鎖 | 1 | 01 |
synchronized優化
jdk1.6爲了減少獲得鎖和釋放鎖帶來的性能消耗,對synchronized獲取鎖進行了優化。
無鎖->偏向鎖->輕量級鎖->重量級鎖。
1、偏向鎖
大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,爲了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程訪問同步塊並獲取鎖時,會在對象頭的鎖記錄裏存儲鎖偏向的線程ID,以後該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的MarkWord裏是否存儲着指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下MarkWord中偏向鎖的標識是否設置成1(表示當前是偏向鎖):如果沒有設置,則使用CAS競爭鎖;如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。
2、輕量級鎖
輕量級鎖加鎖:
線程在執行同步塊之前,JVM會現在當前線程的棧幀中創建用於存儲鎖記錄的空間,並將對象頭中的MarkWord複製到鎖記錄中,官方稱爲Displaced Mark Word。然後線程嘗試使用CAS將對象頭中的MarkWord替換爲指向鎖記錄的指針。如果成功,當前線程獲得鎖。如果失敗,表示有其它線程競爭,當前線程便嘗試使用自旋來獲取鎖。
輕量級鎖解鎖:
輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word 替換回對象頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹爲重量級鎖。
圖爲兩個線程同時爭奪鎖,導致鎖膨脹。
3、重量鎖
由於自旋會消耗CPU,爲了避免無用的自旋,一旦鎖升級爲重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態,其它線程試圖獲取鎖,都會被阻塞住。