底層實現
synchronized底層實現
詳細參考:楊曉峯極客時間上的課程《Java核心技術面試精講》:第16講 | synchronized底層如何實現?什麼是鎖的升級、降級
-
synchronized 代碼塊是由一對兒 monitorenter/monitorexit 指令實現的,Monitor 對象是同步的基本實現單元。
-
發展歷程:
- JDK6之前,Monitor 的實現完全是依靠操作系統內部的互斥鎖,因爲需要進行用戶態到內核態的切換,所以同步操作是一個無差別的重量級操作。(@所以說效率低下嘛)
- 現代的(Oracle)JDK 中,JVM 對此進行了大刀闊斧地改進,提供了三種不同的 Monitor 實現,也就是常說的三種不同的鎖:偏斜鎖(Biased Locking)、輕量級鎖和重量級鎖大大改進了其性能。
-
偏斜鎖:JVM 會利用 CAS 操作(compare and swap),在對象頭上的 Mark Word 部分設置線程 ID,以表示這個對象偏向於當前線程,所以並不涉及真正的互斥鎖
-
輕量級鎖:
- 如果有另外的線程試圖鎖定某個已經被偏斜過的對象,JVM 就需要撤銷(revoke)偏斜鎖,並切換到輕量級鎖實現。輕量級鎖依賴 CAS 操作 Mark Word 來試圖獲取鎖,如果重試成功,就使用普通的輕量級鎖;否則,進一步升級爲重量級鎖。
- 因爲重量級鎖性能差,所以輕量級鎖又衍生出了一種鎖:自旋鎖,其實現就是自循環若干次,通過CAS操作MARK WROD試圖獲取鎖
其他鎖模型
詳細參考:王寶令極客時間上的課程《Java併發編程實戰》- 08 | 管程:併發編程的萬能鑰匙
- 管程模型:
- Hasen模型:要求 notify() 放在代碼的最後,這樣 T2 通知完 T1 後,T2 就結束了,然後 T1 再執行,這樣就能保證同一時刻只有一個線程執行。
- Hoare模型:T2 通知完 T1 後,T2 阻塞,T1 馬上執行;等 T1 執行完,再喚醒 T2,也能保證同一時刻只有一個線程執行。但是相比 Hasen 模型,T2 多了一次阻塞喚醒操作。
- MESA模型(JAVA參考實現):MESA 管程裏面,T2 通知完 T1 後,T2 還是會接着執行,T1 並不立即執行,僅僅是從條件變量的等待隊列進到入口等待隊列裏面。這樣做的好處是 notify() 不用放到代碼的最後,T2 也沒有多餘的阻塞喚醒操作。但是也有個副作用,就是當 T1 再次執行的時候,可能曾經滿足的條件,現在已經不滿足了,所以需要以循環方式檢驗條件變量。—也就是產生假喚醒
鎖變化
升級/膨脹
其實就是偏斜鎖=》輕量級鎖=》重量級鎖的過程,見第一節 #底層實現
鎖降級
鎖降級確實是會發生的,當 JVM 進入安全點(SafePoint)的時候,會檢查是否有閒置的 Monitor,然後試圖進行降級。
synchronized鎖的範圍
- 範圍
- 代碼塊
- 方法
- 對象
- 類
- 對象鎖和類
- 類鎖和對象鎖是分開的,(現在只是個概念,用來區分對象鎖的,是指靜態方法的鎖),程序中獲得類鎖的同時也可以獲得對象鎖。
- 同一個類鎖和同一個類鎖是互斥的,同一個對象鎖和同一個對象鎖互斥。 非靜態方法不受類鎖的影響
- 對象鎖與實例對象相關, 不同的對象的對象鎖不一樣,可以同時獲取兩個不同對象的對象鎖
package com.keven;
//類鎖和對象鎖的測試代碼
public class SyncTest {
public static void main(String[] args) throws Exception {
runObjectLockTest();
System.out.println("finished runObjectLockTest");
runClassLockTest();
System.out.println("finished runClassLockTest");
runClassObjectLockTest();
System.out.println("finished runClassObjectLockTest");
Thread.sleep(10000);
}
//測試對象鎖和類鎖是否能夠同時獲取, 可以看到兩個線程打印數據不受影響,說明不是同一個鎖
private static void runClassObjectLockTest() {
new Thread(SyncTest::testClassLock1, "thread1").start();
new Thread(() -> {
new SyncTest().testObjectLock();
}, "thread2").start();
}
//測試類鎖,顯示thread1打印完成,後面thread2纔開始打印,從側面驗證獲取到的是同一個鎖
private static void runClassLockTest() {
new Thread(SyncTest::testClassLock1, "thread1").start();
new Thread(SyncTest::testClassLock2, "thread2").start();
}
//測試對象鎖, 可以看到兩個線程打印數據不受影響, 且this對象的hash值不一樣
private static void runObjectLockTest() {
final SyncTest syncTest = new SyncTest();
final Thread thread1 = new Thread(() -> {
syncTest.testObjectLock();
}, "thread1");
final Thread thread2 = new Thread(() -> {
new SyncTest().testObjectLock();
}, "thread2");
thread1.start();
thread2.start();
}
private static synchronized void testClassLock1() {
int i = 100;
int count = 0;
while ((i-- > 0) && (count++ < 10)) {
System.out.println("method testClassLock1--" + Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(50);
} catch (InterruptedException ie) {
}
}
}
private static synchronized void testClassLock2() {
int i = 100;
int count = 0;
while ((i-- > 0) && (count++ < 10)) {
System.out.println("method testClassLock2--" + Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(50);
} catch (InterruptedException ie) {
}
}
}
private void testObjectLock() {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " : " + this);
int i = 100;
int count = 0;
while ((i-- > 0) && (count++ < 5)) {
System.out.println("method testObjectLock--" + Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(50);
} catch (InterruptedException ie) {
}
}
}
}
}
synchronized和ReentrantLock有什麼區別?
詳細可參考:楊曉峯極客時間上的課程《Java核心技術面試精講》:第15講 | synchronized和ReentrantLock有什麼區別呢?
- synchronized 和 ReentrantLock 的性能不能一概而論:
- 早起版本的synchronize在很多場景下性能相差較大
- 在後續版本進行了較多的改進,在低競爭場景中表現可能優於ReentrantLock
- 這裏所謂的公平性是指在競爭場景中,當公平性爲真時,會傾向於將鎖賦予等待時間最久的線程。公平性是減少線程“飢餓”(個別線程長期等待鎖,但始終無法獲取)情況發生的一個辦法。
- ReentrantLock與Synchronized的區別:
- ReentrantLock
- 更加的靈活,但必須手動釋放鎖
- 可通過條件控制同步
- 可被中斷,並拋出中斷異常,釋放鎖
- 可選擇獲取鎖的超時時間,嘗試獲取鎖
- 可選擇是否爲公平鎖
- 只適合代碼塊的鎖
- 更加的靈活,但必須手動釋放鎖
- synchronized
- 無需釋放鎖,自動處理
- 可修飾方法,類,代碼塊
- 非公平鎖,如果阻塞則必須等待cpu調度
- ReentrantLock
- ReentrantLock與Synchronized的共通點:都是獨佔鎖或者說是排它鎖
關聯關鍵詞
- 在上面的代碼中,我用的是 notifyAll() 來實現通知機制,爲什麼不使用 notify() 呢?
- 這二者是有區別的,notify() 是會隨機地通知等待隊列中的一個線程,而 notifyAll() 會通知等待隊列中的所有線程。
- 從感覺上來講,應該是 notify() 更好一些,因爲即便通知所有線程,也只有一個線程能夠進入臨界區。但那所謂的感覺往往都蘊藏着風險,實際上使用 notify() 也很有風險,它的風險在於可能導致某些線程永遠不會被通知到。@隨機的弊病不就是存在永遠不被輪到的弊病麼?這跟非公平鎖的弊病是一個意思
- wait與sleep區別在於:
- wait會釋放所有鎖而sleep不會釋放鎖資源.
- wait只能在同步方法和同步塊中使用,而sleep任何地方都可以
- wait無需捕捉異常,而sleep需要
- sleep是Thread的方法,而wait是Object類的方法;
- sleep方法調用的時候必須指定時間
兩者相同點:都會讓渡CPU執行時間,等待再次調度!。補充關於二者的區別還可以看知乎的這篇帖子
- wait()方法與sleep()方法的不同之處在於,wait()方法會釋放對象的“鎖標誌”。當調用某一對象的wait()方法後,會使當前線程暫停執行,並將當前線程放入對象等待池中,直到調用了notify()方法後,將從對象等待池中移出任意一個線程並放入鎖標誌等待池中,只有鎖標誌等待池中的線程可以獲取鎖標誌,它們隨時準備爭奪鎖的擁有權。當調用了某個對象的notifyAll()方法,會將對象等待池中的所有線程都移動到該對象的鎖標誌等待池。
- sleep()方法需要指定等待的時間,它可以讓當前正在執行的線程在指定的時間內暫停執行,進入阻塞狀態,該方法既可以讓其他同優先級或者高優先級的線程得到執行的機會,也可以讓低優先級的線程得到執行機會。但是sleep()方法不會釋放“鎖標誌”,也就是說如果有synchronized同步塊,其他線程仍然不能訪問共享數據
常見面試題
- synchronized和ReentrantLock的區別 @見筆記
- 鎖什麼時候升級/降級?@見筆記
- 類鎖和對象鎖的區別? @見筆記
- 爲什麼JDK8中ConcurrentHashMap的鎖實現要用CAS+synchronized來取代Segment+ReentrantLock呢?
- @詳細見ConcurrentHashMap 1.8爲什麼要使用CAS+Synchronized取代Segment+ReentrantLock - 羊飛 - 博客園
- 簡單說就是鎖的粒度下降到Node級別了,競爭會比較小,這個時候synchronized的性能要優於ReentrantLock.
- 爲什麼wait必須是在同步塊中的呢?@重看了一遍王寶令的課程,發現這是MESA管程模型的設計範式,硬要解釋的話可以是這樣:
- wait是跟notify, notifyAll配對的, 是和synchronized關鍵字一起使用的
- wait的工作原理就是wait的時候,會進入同步塊(synchronized)所對應的條件等待隊列,在其他地方使用這個關鍵字是不可進入的
- 或者說wait所對應的管程的入口在synchronied處
- wait與sleep區別是什麼? @見上面的筆記