利用synchronized實作同步的基礎:Java中的每一個對象都可以作為鎖。具體表現為以下3種形式。
- 對于普通同步方法,鎖是目前執行個體對象。
- 對于靜态同步方法,鎖是目前類的Class對象。
- 對于同步方法塊,鎖是Synchonized括号裡配置的對象。
synchronized直接修飾方法
public synchronized void test(){
i++;
}
位元組碼中并沒有monitorenter指令和monitorexit指令,取得代之的是ACC_SYNCHRONIZED辨別,JVM通過ACC_SYNCHRONIZED辨別,就可以知道這是一個需要同步的方法,進而執行上述同步的過程,也就是_count加1,這些過程。
synchronized修飾對象
public class SynchronizedTest {
int i = 0;
public void test1(){
synchronized (SynchronizedTest.class){
i++;
}
}
}
javap SynchronizedTest
Synchonized在JVM裡的實作原理:JVM基于進入和退出Monitor對象來實作方法同步和代碼塊同步。monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個monitor與之關聯,當且一個monitor被持有後,它将處于鎖定狀态。線程執行到monitorenter指令時,将會嘗試擷取對象所對應的monitor的所有權,即嘗試獲得對象的鎖。
synchronized用的鎖是存在Java對象頭裡的。如果對象是數組類型,則虛拟機用3個字寬(Word)存儲對象頭,如果對象是非數組類型,則用2字寬存儲對象頭。在32位虛拟機中,1字寬等于4位元組,即32bit。
java對象頭
長度 | 内容 | 說明 |
---|---|---|
32/64bit | Mark Word | 存儲對象的hashCode或鎖資訊等 |
32/64bit | Class Metadata Address | 存儲到對象類型資料的指針 |
32/64bit | Array length | 數組的長度(如果目前對象是數組) |
Java對象頭裡的Mark Word裡預設存儲對象的HashCode、分代年齡和鎖标記位,32位JVM的Mark Word的預設存儲結構如下
鎖狀态 | 25bit | 4bit | 1bit是否是偏向鎖 | 2bit标志位 |
---|---|---|---|---|
無鎖狀态 | 對象的hashCode | 對象分代年齡 | 01 |
在運作期間,Mark Word裡存儲的資料會随着鎖标志位的變化而變化。Mark Word可能變化為存儲以下4種資料
鎖狀态 | 25bit | 4bit | 1bit | 2bit | |
---|---|---|---|---|---|
23bit | 2bit | 是否是偏向鎖 | 鎖标志位 | ||
輕量級鎖 | 指向棧中鎖記錄的指針 | 00 | |||
重量級鎖 | 指向互斥量(重量級鎖)的指針 | 10 | |||
GC标記 | 空 | 11 | |||
偏向鎖 | 線程ID | Epoch | 對象分代年齡 | 1 | 01 |
在64位虛拟機下,Mark Word是64bit大小的,Mark Word的存儲結構
鎖狀态 | 25bit | 31bit | 1bit | 4bit | 1bit | 2bit |
---|---|---|---|---|---|---|
cms_free | 分代年齡 | 偏向鎖 | 鎖标志位 | |||
無鎖 | unused | hashCode | 01 | |||
偏向鎖 | ThreadID(54bit)Epoch(2bit) | 1 | 01 |
Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,在Java SE 1.6中,鎖一共有4種狀态,級别從低到高依次是:無鎖狀态、偏向鎖狀态、輕量級鎖狀态和重量級鎖狀态,這幾個狀态會随着競争情況逐漸更新。鎖可以更新但不能降級,意味着偏向鎖更新成輕量級鎖後不能降級成偏向鎖。這種鎖更新卻不能降級的政策,目的是為了提高獲得鎖和釋放鎖的效率。
偏向鎖
大多數情況下,鎖不僅不存在多線程競争,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程通路同步塊并擷取鎖時,會在對象頭和棧幀中的鎖記錄裡存儲鎖偏向的線程ID,以後該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,隻需簡單地測試一下對象頭的Mark Word裡是否存儲着指向目前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的辨別是否設定成1(表示目前是偏向鎖):如果沒有設定,則使用CAS競争鎖;如果設定了,則嘗試使用CAS将對象頭的偏向鎖指向目前線程。
偏向鎖的擷取
- 通路 Mark Word 中偏向鎖的标志是否設定成 1,鎖的标志位是否是 01 --- 确認為可偏向狀态。
- 如果确認為可偏向狀态,判斷目前線程id 和 對象頭中存儲的線程 ID 是否一緻,如果一緻的話,則執行步驟5,如果不一緻,進入步驟3
- 如果目前線程ID 與對象頭中存儲的線程ID 不一緻的話,則通過 CAS 操作來競争擷取鎖。如果競争成功,則将 Mark Word 中的線程ID 修改為目前線程ID,然後執行步驟5,如果競争失敗,則執行步驟4
- 如果 CAS 擷取偏向鎖失敗,則表示有競争(CAS 擷取偏向鎖失敗則表明至少有其他線程曾經擷取過偏向鎖,因為線程不會主動釋放偏向鎖)。當到達全局安全點(SafePoint)時,會首先暫停擁有偏向鎖的線程,然後檢查持有偏向鎖的線程是否存活(因為可能持有偏向鎖的線程已經執行完畢,但是該線程并不會主動去釋放偏向鎖),如果線程不處于活動狀态,則将對象頭置為無鎖狀态(标志位為01),然後重新偏向新的線程;如果線程仍然活着,撤銷偏向鎖後更新到輕量級鎖的狀态(标志位為00),此時輕量級鎖由原持有偏向鎖的線程持有,繼續執行其同步代碼,而正在競争的線程會進入自旋等待獲得該輕量級鎖。
- 執行同步代碼
偏向鎖的撤銷
偏向鎖使用了一種等到競争出現才釋放鎖的機制,是以當其他線程嘗試競争偏向鎖時,持有偏向鎖的線程才會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有正在執行的位元組碼)。它會首先暫停擁有偏向鎖的線程,然後檢查持有偏向鎖的線程是否活着,如果線程不處于活動狀态,則将對象頭設定成無鎖狀态;如果線程仍然活着,擁有偏向鎖的棧會被執行,周遊偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要麼重新偏向于其他線程,要麼恢複到無鎖或者标記對象不适合作為偏向鎖,最後喚醒暫停的線程。
關閉偏向鎖
偏向鎖在Java 是預設啟用的,但是它在應用程式啟動幾秒鐘之後才激活,如有必要可以使用JVM參數來關閉延遲:-XX:BiasedLockingStartupDelay=0。如果你确定應用程式裡所有的鎖通常情況下處于競争狀态,可以通過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false,那麼程式預設會進入輕量級鎖狀态。
輕量級鎖
輕量級加鎖
線程在執行同步塊之前,JVM會先在目前線程的棧桢中建立用于存儲鎖記錄的空間,并将對象頭中的Mark Word複制到鎖記錄中,官方稱為Displaced Mark Word。然後線程嘗試使用CAS将對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,目前線程獲得鎖,如果失敗,表示其他線程競争鎖,目前線程便嘗試使用自旋來擷取鎖。
輕量級鎖解鎖
輕量級解鎖時,會使用原子的CAS操作将Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競争發生。如果失敗,表示目前鎖存在競争,鎖就會膨脹成重量級鎖。
兩個線程同時争奪鎖,導緻鎖更新的流程圖
因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖更新成重量級鎖,就不會再恢複到輕量級鎖狀态。當鎖處于這個狀态下,其他線程試圖擷取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之争。
鎖的優缺點對比
鎖 | 優點 | 缺點 | 适用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級的差距 | 如果線程間存在鎖競争,會帶來額外的鎖撤銷的消耗 | 适用于隻有一個線程通路的同步塊場景 |
輕量級鎖 | 競争的線程不會阻塞,提高了程式的響應速度 | 如果始終得不到鎖競争的線程,使用自旋會消耗CPU | 追求響應時間 同步塊執行速度非常快 |
重量級鎖 | 競争線程不使用自旋,不會消耗CPU | 線程阻塞,響應時間慢 | 追求吞吐量 同步塊執行速度較長 |
文章來自:java并發程式設計的藝術