底層實(shí)現(xiàn)
synchronized底層實(shí)現(xiàn)
詳細(xì)參考:楊曉峰極客時(shí)間上的課程《Java核心技術(shù)面試精講》:第16講 | synchronized底層如何實(shí)現(xiàn)锐借?什么是鎖的升級(jí)该押、降級(jí)
synchronized 代碼塊是由一對(duì)兒 monitorenter/monitorexit 指令實(shí)現(xiàn)的疗杉,Monitor 對(duì)象是同步的基本實(shí)現(xiàn)單元。
-
發(fā)展歷程:
- JDK6之前蚕礼,Monitor 的實(shí)現(xiàn)完全是依靠操作系統(tǒng)內(nèi)部的互斥鎖烟具,因?yàn)樾枰M(jìn)行用戶態(tài)到內(nèi)核態(tài)的切換,所以同步操作是一個(gè)無(wú)差別的重量級(jí)操作奠蹬。(@所以說(shuō)效率低下嘛)
- 現(xiàn)代的(Oracle)JDK 中朝聋,JVM 對(duì)此進(jìn)行了大刀闊斧地改進(jìn),提供了三種不同的 Monitor 實(shí)現(xiàn)罩润,也就是常說(shuō)的三種不同的鎖:偏斜鎖(Biased Locking)玖翅、輕量級(jí)鎖和重量級(jí)鎖大大改進(jìn)了其性能翼馆。
偏斜鎖:JVM 會(huì)利用 CAS 操作(compare and swap)割以,在對(duì)象頭上的 Mark Word 部分設(shè)置線程 ID,以表示這個(gè)對(duì)象偏向于當(dāng)前線程应媚,所以并不涉及真正的互斥鎖
-
輕量級(jí)鎖:
- 如果有另外的線程試圖鎖定某個(gè)已經(jīng)被偏斜過(guò)的對(duì)象严沥,JVM 就需要撤銷(xiāo)(revoke)偏斜鎖,并切換到輕量級(jí)鎖實(shí)現(xiàn)中姜。輕量級(jí)鎖依賴(lài) CAS 操作 Mark Word 來(lái)試圖獲取鎖消玄,如果重試成功,就使用普通的輕量級(jí)鎖丢胚;否則翩瓜,進(jìn)一步升級(jí)為重量級(jí)鎖。
- 因?yàn)橹亓考?jí)鎖性能差携龟,所以輕量級(jí)鎖又衍生出了一種鎖:自旋鎖兔跌,其實(shí)現(xiàn)就是自循環(huán)若干次,通過(guò)CAS操作MARK WROD試圖獲取鎖
其他鎖模型
詳細(xì)參考:王寶令極客時(shí)間上的課程《Java并發(fā)編程實(shí)戰(zhàn)》- 08 | 管程:并發(fā)編程的萬(wàn)能鑰匙
- 管程模型:
- Hasen模型:要求 notify() 放在代碼的最后峡蟋,這樣 T2 通知完 T1 后坟桅,T2 就結(jié)束了,然后 T1 再執(zhí)行蕊蝗,這樣就能保證同一時(shí)刻只有一個(gè)線程執(zhí)行仅乓。
- Hoare模型:T2 通知完 T1 后,T2 阻塞蓬戚,T1 馬上執(zhí)行夸楣;等 T1 執(zhí)行完,再喚醒 T2,也能保證同一時(shí)刻只有一個(gè)線程執(zhí)行豫喧。但是相比 Hasen 模型洞慎,T2 多了一次阻塞喚醒操作。
- MESA模型(JAVA參考實(shí)現(xiàn)):MESA 管程里面嘿棘,T2 通知完 T1 后劲腿,T2 還是會(huì)接著執(zhí)行,T1 并不立即執(zhí)行鸟妙,僅僅是從條件變量的等待隊(duì)列進(jìn)到入口等待隊(duì)列里面焦人。這樣做的好處是 notify() 不用放到代碼的最后,T2 也沒(méi)有多余的阻塞喚醒操作重父。但是也有個(gè)副作用花椭,就是當(dāng) T1 再次執(zhí)行的時(shí)候,可能曾經(jīng)滿足的條件房午,現(xiàn)在已經(jīng)不滿足了矿辽,所以需要以循環(huán)方式檢驗(yàn)條件變量」幔—也就是產(chǎn)生假喚醒
鎖變化
升級(jí)/膨脹
其實(shí)就是偏斜鎖=》輕量級(jí)鎖=》重量級(jí)鎖的過(guò)程袋倔,見(jiàn)第一節(jié) #底層實(shí)現(xiàn)
鎖降級(jí)
鎖降級(jí)確實(shí)是會(huì)發(fā)生的,當(dāng) JVM 進(jìn)入安全點(diǎn)(SafePoint)的時(shí)候折柠,會(huì)檢查是否有閑置的 Monitor宾娜,然后試圖進(jìn)行降級(jí)。
synchronized鎖的范圍
- 范圍
- 代碼塊
- 方法
- 對(duì)象
- 類(lèi)
- 對(duì)象鎖和類(lèi)
- 類(lèi)鎖和對(duì)象鎖是分開(kāi)的扇售,(現(xiàn)在只是個(gè)概念前塔,用來(lái)區(qū)分對(duì)象鎖的,是指靜態(tài)方法的鎖)承冰,程序中獲得類(lèi)鎖的同時(shí)也可以獲得對(duì)象鎖华弓。
- 同一個(gè)類(lèi)鎖和同一個(gè)類(lèi)鎖是互斥的,同一個(gè)對(duì)象鎖和同一個(gè)對(duì)象鎖互斥困乒。 非靜態(tài)方法不受類(lèi)鎖的影響
- 對(duì)象鎖與實(shí)例對(duì)象相關(guān)寂屏, 不同的對(duì)象的對(duì)象鎖不一樣,可以同時(shí)獲取兩個(gè)不同對(duì)象的對(duì)象鎖
package com.keven;
//類(lèi)鎖和對(duì)象鎖的測(cè)試代碼
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);
}
//測(cè)試對(duì)象鎖和類(lèi)鎖是否能夠同時(shí)獲取, 可以看到兩個(gè)線程打印數(shù)據(jù)不受影響顶燕,說(shuō)明不是同一個(gè)鎖
private static void runClassObjectLockTest() {
new Thread(SyncTest::testClassLock1, "thread1").start();
new Thread(() -> {
new SyncTest().testObjectLock();
}, "thread2").start();
}
//測(cè)試類(lèi)鎖凑保,顯示thread1打印完成,后面thread2才開(kāi)始打印涌攻,從側(cè)面驗(yàn)證獲取到的是同一個(gè)鎖
private static void runClassLockTest() {
new Thread(SyncTest::testClassLock1, "thread1").start();
new Thread(SyncTest::testClassLock2, "thread2").start();
}
//測(cè)試對(duì)象鎖, 可以看到兩個(gè)線程打印數(shù)據(jù)不受影響欧引, 且this對(duì)象的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有什么區(qū)別?
詳細(xì)可參考:楊曉峰極客時(shí)間上的課程《Java核心技術(shù)面試精講》:第15講 | synchronized和ReentrantLock有什么區(qū)別呢恳谎?
- synchronized 和 ReentrantLock 的性能不能一概而論:
- 早起版本的synchronize在很多場(chǎng)景下性能相差較大
- 在后續(xù)版本進(jìn)行了較多的改進(jìn)芝此,在低競(jìng)爭(zhēng)場(chǎng)景中表現(xiàn)可能由于ReentrantLock
- 這里所謂的公平性是指在競(jìng)爭(zhēng)場(chǎng)景中憋肖,當(dāng)公平性為真時(shí),會(huì)傾向于將鎖賦予等待時(shí)間最久的線程婚苹。公平性是減少線程“饑餓”(個(gè)別線程長(zhǎng)期等待鎖岸更,但始終無(wú)法獲取)情況發(fā)生的一個(gè)辦法膊升。
- ReentrantLock與Synchronized的區(qū)別:
- ReentrantLock
- 更加的靈活怎炊,但必須手動(dòng)釋放鎖
- 可通過(guò)條件控制同步
- 可被中斷,并拋出中斷異常廓译,釋放鎖
- 可選擇獲取鎖的超時(shí)時(shí)間评肆,嘗試獲取鎖
- 可選擇是否為公平鎖
- 只適合代碼塊的鎖
- 更加的靈活怎炊,但必須手動(dòng)釋放鎖
- synchronized
- 無(wú)需釋放鎖,自動(dòng)處理
- 可修飾方法非区,類(lèi)瓜挽,代碼塊
- 非公平鎖,如果阻塞則必須等待cpu調(diào)度
- ReentrantLock
- ReentrantLock與Synchronized的共通點(diǎn):都是獨(dú)占鎖或者說(shuō)是排它鎖
關(guān)聯(lián)關(guān)鍵詞
- 在上面的代碼中征绸,我用的是 notifyAll() 來(lái)實(shí)現(xiàn)通知機(jī)制久橙,為什么不使用 notify() 呢?
- 這二者是有區(qū)別的管怠,notify() 是會(huì)隨機(jī)地通知等待隊(duì)列中的一個(gè)線程淆衷,而 notifyAll() 會(huì)通知等待隊(duì)列中的所有線程。
- 從感覺(jué)上來(lái)講排惨,應(yīng)該是 notify() 更好一些吭敢,因?yàn)榧幢阃ㄖ芯€程,也只有一個(gè)線程能夠進(jìn)入臨界區(qū)暮芭。但那所謂的感覺(jué)往往都蘊(yùn)藏著風(fēng)險(xiǎn),實(shí)際上使用 notify() 也很有風(fēng)險(xiǎn)欲低,它的風(fēng)險(xiǎn)在于可能導(dǎo)致某些線程永遠(yuǎn)不會(huì)被通知到辕宏。@隨機(jī)的弊病不就是存在永遠(yuǎn)不被輪到的弊病么?這跟非公平鎖的弊病是一個(gè)意思
- wait與sleep區(qū)別在于:
- wait會(huì)釋放所有鎖而sleep不會(huì)釋放鎖資源.
- wait只能在同步方法和同步塊中使用砾莱,而sleep任何地方都可以
- wait無(wú)需捕捉異常瑞筐,而sleep需要
- sleep是Thread的方法,而wait是Object類(lèi)的方法腊瑟;
- sleep方法調(diào)用的時(shí)候必須指定時(shí)間
兩者相同點(diǎn):都會(huì)讓渡CPU執(zhí)行時(shí)間聚假,等待再次調(diào)度!闰非。補(bǔ)充關(guān)于二者的區(qū)別還可以看知乎的這篇帖子
- wait()方法與sleep()方法的不同之處在于膘格,wait()方法會(huì)釋放對(duì)象的“鎖標(biāo)志”。當(dāng)調(diào)用某一對(duì)象的wait()方法后财松,會(huì)使當(dāng)前線程暫停執(zhí)行瘪贱,并將當(dāng)前線程放入對(duì)象等待池中纱控,直到調(diào)用了notify()方法后,將從對(duì)象等待池中移出任意一個(gè)線程并放入鎖標(biāo)志等待池中菜秦,只有鎖標(biāo)志等待池中的線程可以獲取鎖標(biāo)志甜害,它們隨時(shí)準(zhǔn)備爭(zhēng)奪鎖的擁有權(quán)。當(dāng)調(diào)用了某個(gè)對(duì)象的notifyAll()方法球昨,會(huì)將對(duì)象等待池中的所有線程都移動(dòng)到該對(duì)象的鎖標(biāo)志等待池尔店。
- sleep()方法需要指定等待的時(shí)間,它可以讓當(dāng)前正在執(zhí)行的線程在指定的時(shí)間內(nèi)暫停執(zhí)行主慰,進(jìn)入阻塞狀態(tài)闹获,該方法既可以讓其他同優(yōu)先級(jí)或者高優(yōu)先級(jí)的線程得到執(zhí)行的機(jī)會(huì),也可以讓低優(yōu)先級(jí)的線程得到執(zhí)行機(jī)會(huì)河哑。但是sleep()方法不會(huì)釋放“鎖標(biāo)志”避诽,也就是說(shuō)如果有synchronized同步塊,其他線程仍然不能訪問(wèn)共享數(shù)據(jù)
常見(jiàn)面試題
- synchronized和ReentrantLock的區(qū)別 @見(jiàn)筆記
- 鎖什么時(shí)候升級(jí)/降級(jí)璃谨?@見(jiàn)筆記
- 類(lèi)鎖和對(duì)象鎖的區(qū)別沙庐? @見(jiàn)筆記
- 為什么JDK8中ConcurrentHashMap的鎖實(shí)現(xiàn)要用CAS+synchronized來(lái)取代Segment+ReentrantLock呢?
- @詳細(xì)見(jiàn)ConcurrentHashMap 1.8為什么要使用CAS+Synchronized取代Segment+ReentrantLock - 羊飛 - 博客園
- 簡(jiǎn)單說(shuō)就是鎖的粒度下降到Node級(jí)別了佳吞,競(jìng)爭(zhēng)會(huì)比較小拱雏,這個(gè)時(shí)候synchronized的性能要優(yōu)于ReentrantLock.
- 為什么wait必須是在同步塊中的呢?@重看了一遍王寶令的課程底扳,發(fā)現(xiàn)這是MESA管程模型的設(shè)計(jì)范式铸抑,硬要解釋的話可以是這樣:
- wait是跟notify, notifyAll配對(duì)的, 是和synchronized關(guān)鍵字一起使用的
- wait的工作原理就是wait的時(shí)候衷模,會(huì)進(jìn)入同步塊(synchronized)所對(duì)應(yīng)的條件等待隊(duì)列鹊汛,在其他地方使用這個(gè)關(guān)鍵字是不可進(jìn)入的
- 或者說(shuō)wait所對(duì)應(yīng)的管程的入口在synchronied處
- wait與sleep區(qū)別是什么? @見(jiàn)上面的筆記