Java Synchronized 鎖的實現原理與應用 (偏向鎖,輕量鎖,重量鎖)
在Java SE 1.6之前,Synchronized被稱為重量級鎖.在SE 1.6之後進行了各種優化,就出現了偏向鎖,輕量鎖,目的是為了減少獲得鎖和釋放鎖帶來的性能消耗.
Synchroized的使用(三種形式)
(1) 對於普通同步方法,鎖是當前實例對象.如下代碼示例:
解釋:對於set和get方法來說,都是在方法上使用了同步關鍵字,所以他們是同步方法,鎖的就是當前的實例對象,怎麽理解了,看下面的main方法,就是這個new的實例對象.所以他們的鎖對象都是synchronizedMethod 這個實例.
private int i = 0; public synchronized void setNum (int number) { this.i = number; } public synchronized int getNum () { return i; } public static void main (String[] args) { // 啟動兩個線程調用get和set方法 SynchronizedMethod synchronizedMethod = new SynchronizedMethod(); new Thread(() -> { synchronizedMethod.setNum(5); },"set").start(); new Thread(() -> { int num = synchronizedMethod.getNum(); System.out.println(num); },"get").start(); }
(2) 對於靜態同步方法,鎖是當前類的Class對象.看代碼示例:
解釋:如下兩個方法都是靜態同步方法.所以鎖是當前類的class對象,這麽理解吧,靜態方法是類調用的,所以鎖就是這個類對象.如下代碼運行結果,只有當類的第一個靜態同步方法執行完畢,第二個才能執行.
/** * synchronized 靜態方法 */ public class SynchroizedStaticMethod { private static int i = 0; public static synchronized void addNum () { for (;;) { i++; System.out.println(Thread.currentThread().getName()+"----"+i); if(i >= 100){ break; } } } public static synchronized void getNum () { System.out.println(Thread.currentThread().getName()+"----"+i); } public static void main (String[] args) { new Thread(() -> { SynchroizedStaticMethod.addNum(); },"addNum").start(); new Thread(() -> { SynchroizedStaticMethod.getNum(); },"getNum").start(); } }
一部分執行結果
addNum----92
addNum----93
addNum----94
addNum----95
addNum----96
addNum----97
addNum----98
addNum----99
addNum----100
getNum----100
Process finished with exit code 0
(3) 對於同步代碼塊,鎖就是Synchronized括號裏面配置的對象.如下代碼實例:
解釋:通過如下代碼可以證明鎖就是括號裏面的對象,當兩個方法是一個對象時,只能是獲取到對象鎖的方法 執行,但是是兩個鎖對象時,那麽兩個方法獲取的就是不同的鎖對象,所以結果不一樣了.
/** * 代碼塊 */ public class SynchroizedCodeBlock { private Object object = new Object(); public void printOne () { synchronized (object) { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "---" + 1); } } } public void printTwo () { synchronized (object) { System.out.println(Thread.currentThread().getName()+"---"+2); } } public static void main (String[] args) { SynchroizedCodeBlock codeBlock = new SynchroizedCodeBlock(); new Thread(() -> { codeBlock.printOne(); },"printOne").start(); new Thread(() -> { codeBlock.printTwo(); },"printTwo").start(); } }
執行結果
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printTwo---2
Process finished with exit code 0
改變下括號裏面的對象
public void printTwo () {
synchronized (this) {
System.out.println(Thread.currentThread().getName()+"---"+2);
}
}
執行結果(與第一次不一樣了)
printTwo---2
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
Process finished with exit code 0
3.鎖在什麽地方(Java 對象頭)
Synchronized用的鎖是存在Java的對象頭裏的.如果對象時數組類型,則虛擬機用3個字寬存儲對象頭..Java對象頭裏的Mark Word裏默認儲存對象的HashCode.分代年齡和鎖標記位
長度 | 內容 | 說明 |
---|---|---|
32/64bit | Mark Word | 存儲對象的hashcode或鎖信息等 |
32/64bit | Class Metadata Address | 存儲對象數據類型的指針 |
32/64bit | Array length | 數組的長度(如果當前對象時數組) |
Mark Word 的狀態變化表
4.JSE1.6對鎖的優化(鎖的升級與對比)
在Java SE1.6中,鎖一共有4中狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。
(1)偏向鎖
why:在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一個線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖.
what:當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏儲存偏向的線程ID,以後該線程在進入和退出同步代碼塊時不需要進行cas操作來加鎖和解鎖,只需要簡單地測試一下對象頭的Mark Word裏是否儲存著指向當前線程的偏向鎖。如果測試成功,表示該線程獲得了鎖。如果測試失敗,則需要在測試一下Mark Word中偏向鎖的表示是否設置成1(表示當前是偏向鎖):如果沒有設置,則使用cas競爭鎖;如果設置了,則嘗試使用cas將對象頭的偏向鎖指向當前線程。
偏向鎖的撤銷:偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其它線程嘗試競爭偏向鎖時,持有線程才會釋放鎖。偏向鎖的撤銷,需要等待全局安全節點(在這個時間點上沒有正在執行的字節碼)。
偏向鎖的升級:如果有線程來競爭偏向鎖,那麽就需要判斷對象頭的Mark Word的線程ID和當前線程ID是否一致,如果不一致說明發送了競爭,那麽就需要檢查擁有偏向鎖的線程是否還存活;如果沒有存活,那麽將對象頭設置為無鎖狀態,當前線程和其它線程都可以去競爭偏向鎖;如果存活,暫停擁有偏向鎖的線程,遍歷棧幀信息,判斷這個線程是否還要使用這個鎖對象,如果還需要,就撤銷偏向鎖,升級為輕量鎖,如果不要繼續使用,標記為無鎖狀態,重新偏向其它線程。如果升級為輕量鎖後,應該還是擁有鎖的線程先去執行。
(2) 輕量鎖
why:輕量鎖是為線程競爭不是很多,每個線程的執行時間不長而準備的,因為輕量鎖發生競爭時,不阻塞線程,而是采用的自旋;如果競爭時就阻塞線程,而鎖很快就釋放了,這個線程的狀態切換也是很大的消耗。
waht:線程在執行同步代碼塊前,jvm會先在當前線程的棧幀中創建一個用於存儲鎖記錄的空間,並將對象頭中的Mark Word替換為為指向鎖記錄的指針。如果成功,當前線程獲取鎖,如果失敗,表示其它線程競爭鎖,當前線程嘗試使用自旋來獲取鎖。這一塊其實有些繞,就是怎麽判斷鎖這一塊具體參考這篇文檔
輕量鎖的解鎖:輕量級解鎖時,會使用cas操作將disolaced Mark Word替換回到對象頭,如果成功,則表示沒有發生競爭。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。過程如下圖所示:
(3) 鎖的優缺點對比
鎖 | 優點 | 缺點 | 使用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要額為的消耗,和執行非同步方法相比,僅存在納秒級別的差距 | 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 | 適用於只有一個線程訪問的同步塊場景 |
輕量鎖 | 競爭線程不會阻塞,提高了程序的響應速度 | 如果始終得不到鎖競爭的線程,使用自旋會消耗cpu | 追求響應時間,同步塊執行速度非常快 |
重量級鎖 | 線程競爭不使用自旋,不消耗cpu | 線程阻塞,響應時間緩慢 | 追求吞吐量,同步塊執行速度較長 |
Java Synchronized 鎖的實現原理與應用 (偏向鎖,輕量鎖,重量鎖)