synchronized鎖的原理也是大廠面試中經(jīng)常會(huì)涉及的問題,本文主要通過對(duì)以下問題進(jìn)行分析講解,來幫助大家理解synchronized鎖的原理。
1.synchronized鎖是什么?鎖的對(duì)象是什么?
2.偏向鎖,輕量級(jí)鎖,重量級(jí)鎖的執(zhí)行流程是怎樣的?
3.為什么說是輕量級(jí),重量級(jí)鎖是不公平的?
4.重量級(jí)鎖為什么需要自旋操作?
5.什么時(shí)候會(huì)發(fā)生鎖升級(jí),鎖降級(jí)?
6.偏向鎖,輕量鎖,重量鎖的適用場(chǎng)景,優(yōu)缺點(diǎn)是什么?
1.synchronized鎖是什么?鎖的對(duì)象是什么?
synchronized的英文意思就是同步的意思,就是可以讓synchronized修飾的方法,代碼塊,每次只能有一個(gè)線程在執(zhí)行,以此來實(shí)現(xiàn)數(shù)據(jù)的安全。
一般可以修飾同步代碼塊、實(shí)例方法、靜態(tài)方法,加鎖對(duì)象分別為同步代碼塊塊括號(hào)內(nèi)的對(duì)象、實(shí)例對(duì)象、類。
在實(shí)現(xiàn)原理上,
synchronized修飾同步代碼塊,javac在編譯時(shí),在synchronized同步塊的進(jìn)入的指令前和退出的指令后,會(huì)分別生成對(duì)應(yīng)的monitorenter和monitorexit指令進(jìn)行對(duì)應(yīng),代表嘗試獲取鎖和釋放鎖。(為了保證拋異常的情況下也能釋放鎖,所以javac為同步代碼塊添加了一個(gè)隱式的try-finally,在finally中會(huì)調(diào)用monitorexit命令釋放鎖。)
synchronized修飾方法,javac為方法的flags屬性添加了一個(gè)ACCSYNCHRONIZED關(guān)鍵字,在JVM進(jìn)行方法調(diào)用時(shí),發(fā)現(xiàn)調(diào)用的方法被ACCSYNCHRONIZED修飾,則會(huì)先嘗試獲得鎖。
public class SyncTest {
? ? private Object lockObject = new Object();
? ? public void syncBlock(){
? ? ? ? //修飾代碼塊,加鎖對(duì)象為lockObject
? ? ? ? synchronized (lockObject){
? ? ? ? ? ? System.out.println("hello block");
? ? ? ? }
? ? }
? ? //修飾實(shí)例方法,加鎖對(duì)象為當(dāng)前的實(shí)例對(duì)象
? ? public synchronized void syncMethod(){
? ? ? ? System.out.println("hello method");
? ? }
? ? //修飾靜態(tài)方法,加鎖對(duì)象為當(dāng)前的類
? ? public static synchronized void staticSyncMethod(){
? ? ? ? System.out.println("hello method");
? ? }
}
2.偏向鎖,輕量級(jí)鎖,重量級(jí)鎖的執(zhí)行流程是怎樣的?
在JVM中,一個(gè)Java對(duì)象其實(shí)由對(duì)象頭+實(shí)例數(shù)據(jù)+對(duì)齊填充三部分組成,而對(duì)象頭主要包含Mark Word+指向?qū)ο笏鶎俚念惖闹羔樈M成(如果是數(shù)組對(duì)象,還會(huì)包含長(zhǎng)度)。像下圖一樣:
Mark Word:存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),例如hashCode,GC分代年齡,鎖狀態(tài)標(biāo)志,線程持有的鎖等等。在32位系統(tǒng)占4字節(jié),在64位系統(tǒng)中占8字節(jié),所以它能存儲(chǔ)的數(shù)據(jù)量是有限的,所以主要通過設(shè)立是否偏向鎖的標(biāo)志位和鎖標(biāo)志位用于區(qū)分其他位數(shù)存儲(chǔ)的數(shù)據(jù)是什么,具體請(qǐng)看下圖:
鎖信息都是存在鎖對(duì)象的Mark Word中的,當(dāng)對(duì)象狀態(tài)為偏向鎖時(shí),Mark Word存儲(chǔ)的是偏向的線程ID;當(dāng)狀態(tài)為輕量級(jí)鎖時(shí),Mark Word存儲(chǔ)的是指向線程棧中LockRecord的指針;當(dāng)狀態(tài)為重量級(jí)鎖時(shí),Mark Word為指向堆中的monitor對(duì)象的指針。
這是網(wǎng)上找到的一個(gè)流程圖,可以先看流程圖,結(jié)合著文字來了解執(zhí)行流程
偏向鎖
Hotspot的作者經(jīng)過以往的研究發(fā)現(xiàn)大多數(shù)情況下鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得,于是引入了偏向鎖。
簡(jiǎn)單的來說,就是主要鎖處于偏向鎖狀態(tài)時(shí),會(huì)在Mark Word中存當(dāng)前持有偏向鎖的線程ID,如果獲取鎖的線程ID與它一致就說明是同一個(gè)線程,可以直接執(zhí)行,不用像輕量級(jí)鎖那樣執(zhí)行CAS操作來加鎖和解鎖。
偏向鎖的加鎖過程:
場(chǎng)景一:當(dāng)鎖對(duì)象第一次被線程獲得鎖的時(shí)候
線程發(fā)現(xiàn)是匿名偏向狀態(tài)(也就是鎖對(duì)象的Mark Word沒有存儲(chǔ)線程ID),則會(huì)用CAS指令,將mark word中的thread id由0改成當(dāng)前線程Id。如果成功,則代表獲得了偏向鎖,繼續(xù)執(zhí)行同步塊中的代碼。否則,將偏向鎖撤銷,升級(jí)為輕量級(jí)鎖。
場(chǎng)景二:當(dāng)獲取偏向鎖的線程再次進(jìn)入同步塊時(shí)
發(fā)現(xiàn)鎖對(duì)象存儲(chǔ)的線程ID就是當(dāng)前線程的ID,會(huì)往當(dāng)前線程的棧中添加一條DisplacedMarkWord為空的LockRecord中,然后繼續(xù)執(zhí)行同步塊的代碼,因?yàn)椴倏v的是線程私有的棧,因此不需要用到CAS指令;由此可見偏向鎖模式下,當(dāng)被偏向的線程再次嘗試獲得鎖時(shí),僅僅進(jìn)行幾個(gè)簡(jiǎn)單的操作就可以了,在這種情況下,synchronized關(guān)鍵字帶來的性能開銷基本可以忽略。
場(chǎng)景二:當(dāng)沒有獲得鎖的線程進(jìn)入同步塊時(shí)
當(dāng)沒有獲得鎖的線程進(jìn)入同步塊時(shí),發(fā)現(xiàn)當(dāng)前是偏向鎖狀態(tài),并且存儲(chǔ)的是其他線程ID(也就是其他線程正在持有偏向鎖),則會(huì)進(jìn)入到撤銷偏向鎖的邏輯里,一般來說,會(huì)在safepoint中去查看偏向的線程是否還存活
如果線程存活且還在同步塊中執(zhí)行, 則將鎖升級(jí)為輕量級(jí)鎖,原偏向的線程繼續(xù)擁有鎖,只不過持有的是輕量級(jí)鎖,繼續(xù)執(zhí)行代碼塊,執(zhí)行完之后按照輕量級(jí)鎖的解鎖方式進(jìn)行解鎖,而其他線程則進(jìn)行自旋,嘗試獲得輕量級(jí)鎖。
如果偏向的線程已經(jīng)不存活或者不在同步塊中, 則將對(duì)象頭的mark word改為無鎖狀態(tài)(unlocked)
由此可見,偏向鎖升級(jí)的時(shí)機(jī)為:當(dāng)一個(gè)線程獲得了偏向鎖,在執(zhí)行時(shí),只要有另一個(gè)線程嘗試獲得偏向鎖,并且當(dāng)前持有偏向鎖的線程還在同步塊中執(zhí)行,則該偏向鎖就會(huì)升級(jí)成輕量級(jí)鎖。
偏向鎖的解鎖過程
因此偏向鎖的解鎖很簡(jiǎn)單,其僅僅將線程的棧中的最近一條lockrecord的obj字段設(shè)置為null。需要注意的是,偏向鎖的解鎖步驟中并不會(huì)修改鎖對(duì)象Mark Word中的thread id,簡(jiǎn)單的說就是鎖對(duì)象處于偏向鎖時(shí),Mark Word中的thread id 可能是正在執(zhí)行同步塊的線程的id,也可能是上次執(zhí)行完已經(jīng)釋放偏向鎖的thread id,主要是為了上次持有偏向鎖的這個(gè)線程在下次執(zhí)行同步塊時(shí),判斷Mark Word中的thread id相同就可以直接執(zhí)行,而不用通過CAS操作去將自己的thread id設(shè)置到鎖對(duì)象Mark Word中。這是偏向鎖執(zhí)行的大概流程:
輕量級(jí)鎖
重量級(jí)鎖依賴于底層的操作系統(tǒng)的Mutex Lock來實(shí)現(xiàn)的,但是由于使用Mutex Lock需要將當(dāng)前線程掛起并從用戶態(tài)切換到內(nèi)核態(tài)來執(zhí)行,這種切換的代價(jià)是非常昂貴的,而在大部分時(shí)候可能并沒有多線程競(jìng)爭(zhēng),只是這段時(shí)間是線程A執(zhí)行同步塊,另外一段時(shí)間是線程B來執(zhí)行同步塊,僅僅是多線程交替執(zhí)行,并不是同時(shí)執(zhí)行,也沒有競(jìng)爭(zhēng),如果采用重量級(jí)鎖效率比較低。以及在重量級(jí)鎖中,沒有獲得鎖的線程會(huì)阻塞,獲得鎖之后線程會(huì)被喚醒,阻塞和喚醒的操作是比較耗時(shí)間的,如果同步塊的代碼執(zhí)行比較快,等待鎖的線程可以進(jìn)行先進(jìn)行自旋操作(就是不釋放CPU,執(zhí)行一些空指令或者是幾次for循環(huán)),等待獲取鎖,這樣效率比較高。所以輕量級(jí)鎖天然瞄準(zhǔn)不存在鎖競(jìng)爭(zhēng)的場(chǎng)景,如果存在鎖競(jìng)爭(zhēng)但不激烈,仍然可以用自旋鎖優(yōu)化,自旋失敗后再升級(jí)為重量級(jí)鎖。
輕量級(jí)鎖的加鎖過程
JVM會(huì)為每個(gè)線程在當(dāng)前線程的棧幀中創(chuàng)建用于存儲(chǔ)鎖記錄的空間,我們稱為Displaced Mark Word。如果一個(gè)線程獲得鎖的時(shí)候發(fā)現(xiàn)是輕量級(jí)鎖,會(huì)把鎖的Mark Word復(fù)制到自己的Displaced Mark Word里面。
然后線程嘗試用CAS操作將鎖的Mark Word替換為自己線程棧中拷貝的鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,如果失敗,表示Mark Word已經(jīng)被替換成了其他線程的鎖記錄,說明在與其它線程競(jìng)爭(zhēng)鎖,當(dāng)前線程就嘗試使用自旋來獲取鎖。
自旋:不斷嘗試去獲取鎖,一般用循環(huán)來實(shí)現(xiàn)。
自旋是需要消耗CPU的,如果一直獲取不到鎖的話,那該線程就一直處在自旋狀態(tài),白白浪費(fèi)CPU資源。
JDK采用了適應(yīng)性自旋,簡(jiǎn)單來說就是線程如果自旋成功了,則下次自旋的次數(shù)會(huì)更多,如果自旋失敗了,則自旋的次數(shù)就會(huì)減少。
自旋也不是一直進(jìn)行下去的,如果自旋到一定程度(和JVM、操作系統(tǒng)相關(guān)),依然沒有獲取到鎖,稱為自旋失敗,那么這個(gè)線程會(huì)阻塞。同時(shí)這個(gè)鎖就會(huì)升級(jí)成重量級(jí)鎖。
輕量級(jí)鎖的釋放流程
在釋放鎖時(shí),當(dāng)前線程會(huì)使用CAS操作將Displaced Mark Word的內(nèi)容復(fù)制回鎖的Mark Word里面。如果沒有發(fā)生競(jìng)爭(zhēng),那么這個(gè)復(fù)制的操作會(huì)成功。如果有其他線程因?yàn)樽孕啻螌?dǎo)致輕量級(jí)鎖升級(jí)成了重量級(jí)鎖,那么CAS操作會(huì)失敗,此時(shí)會(huì)釋放鎖并喚醒被阻塞的線程。輕量級(jí)鎖的加鎖解鎖流程圖:
重量級(jí)鎖
當(dāng)多個(gè)線程同時(shí)請(qǐng)求某個(gè)重量級(jí)鎖時(shí),重量級(jí)鎖會(huì)設(shè)置幾種狀態(tài)用來區(qū)分請(qǐng)求的線程:
Contention List:所有請(qǐng)求鎖的線程將被首先放置到該競(jìng)爭(zhēng)隊(duì)列,我也不知道為什么網(wǎng)上的文章都叫它隊(duì)列,其實(shí)這個(gè)隊(duì)列是先進(jìn)后出的,更像是棧,就是當(dāng)Entry List為空時(shí),Owner線程會(huì)直接從Contention List的隊(duì)列尾部(后加入的線程中)取一個(gè)線程,讓它成為OnDeck線程去競(jìng)爭(zhēng)鎖。(主要是剛來獲取重量級(jí)鎖的線程是會(huì)進(jìn)行自旋操作來獲取鎖,獲取不到才會(huì)進(jìn)入Contention List,所以O(shè)nDeck線程主要與剛進(jìn)來還在自旋,還沒有進(jìn)入到Contention List的線程競(jìng)爭(zhēng))
Entry List:Contention List中那些有資格成為候選人的線程被移到Entry List,主要是為了減少對(duì)Contention List的并發(fā)訪問,因?yàn)榧葧?huì)添加新線程到隊(duì)尾,也會(huì)從隊(duì)尾取線程。
Wait Set:那些調(diào)用wait方法被阻塞的線程被放置到Wait Set。
OnDeck:任何時(shí)刻最多只能有一個(gè)線程正在競(jìng)爭(zhēng)鎖,該線程稱為OnDeck。
Owner:獲得鎖的線程稱為Owner。!Owner:釋放鎖的線程。
重量級(jí)鎖執(zhí)行流程:
流程圖如下:
步驟1是線程在進(jìn)入Contention List時(shí)阻塞等待之前,程會(huì)先嘗試自旋使用CAS操作獲取鎖,如果獲取不到就進(jìn)入Contention List隊(duì)列的尾部。
步驟2是Owner線程在解鎖時(shí),如果Entry List為空,那么會(huì)先將Contention List中隊(duì)列尾部的部分線程移動(dòng)到Entry List。
步驟3是Owner線程在解鎖時(shí),如果Entry List不為空,從Entry List中取一個(gè)線程,讓它成為OnDeck線程,Owner線程并不直接把鎖傳遞給OnDeck線程,而是把鎖競(jìng)爭(zhēng)的權(quán)利交給OnDeck,OnDeck需要重新競(jìng)爭(zhēng)鎖,JVM中這種選擇行為稱為 “競(jìng)爭(zhēng)切換”。(主要是與還沒有進(jìn)入到Contention
List,還在自旋獲取重量級(jí)鎖的線程競(jìng)爭(zhēng))
步驟4就是OnDeck線程獲取到鎖,成為Owner線程進(jìn)行執(zhí)行。
步驟5就是Owner線程調(diào)用鎖對(duì)象的wait()方法進(jìn)行等待,會(huì)移動(dòng)到Wait Set中,并且會(huì)釋放CPU資源,也同時(shí)釋放鎖,
步驟6.就是當(dāng)其他線程調(diào)用鎖對(duì)象的notify()方法,之前調(diào)用wait方法等待的這個(gè)線程才會(huì)從Wait Set移動(dòng)到Entry List,等待獲取鎖。
3.為什么說是輕量級(jí),重量級(jí)鎖是不公平的?
偏向鎖由于不涉及到多個(gè)線程競(jìng)爭(zhēng),所以談不上公平不公平,輕量級(jí)鎖獲取鎖的方式是多個(gè)線程進(jìn)行自旋操作,然后使用用CAS操作將鎖的Mark Word替換為指向自己線程棧中拷貝的鎖記錄的指針,所以誰(shuí)能獲得鎖就看運(yùn)氣,不看先后順序。重量級(jí)鎖不公平主要在于剛進(jìn)入到重量級(jí)的鎖的線程不會(huì)直接進(jìn)入Contention List隊(duì)列,而是自旋去獲取鎖,所以后進(jìn)來的線程也有一定的幾率先獲得到鎖,所以是不公平的。
4.重量級(jí)鎖為什么需要自旋操作?
因?yàn)槟切┨幱贑ontetionList、EntryList、WaitSet中的線程均處于阻塞狀態(tài),阻塞操作由操作系統(tǒng)完成(在Linxu下通過pthreadmutexlock函數(shù))。線程被阻塞后便進(jìn)入內(nèi)核(Linux)調(diào)度狀態(tài),這個(gè)會(huì)導(dǎo)致系統(tǒng)在用戶態(tài)與內(nèi)核態(tài)之間來回切換,嚴(yán)重影響鎖的性能。如果同步塊中代碼比較少,執(zhí)行比較快的話,后進(jìn)來的線程先自旋獲取鎖,先執(zhí)行,而不進(jìn)入阻塞狀態(tài),減少額外的開銷,可以提高系統(tǒng)吞吐量。
5.什么時(shí)候會(huì)發(fā)生鎖升級(jí),鎖降級(jí)?
偏向鎖升級(jí)為輕量級(jí)鎖:就是有不同的線程競(jìng)爭(zhēng)鎖時(shí)。具體來看就是當(dāng)一個(gè)線程發(fā)現(xiàn)當(dāng)前鎖狀態(tài)是偏向鎖,然后鎖對(duì)象存儲(chǔ)的Thread id是其他線程的id,并且去Thread id對(duì)應(yīng)的線程棧查詢到的lock record的obj字段不為null(代表當(dāng)前持有偏向鎖的線程還在執(zhí)行同步塊)。那么該偏向鎖就會(huì)升級(jí)成輕量級(jí)鎖。
輕量級(jí)鎖升級(jí)為重量級(jí)鎖:就是在輕量級(jí)鎖中,沒有獲取到鎖的線程進(jìn)行自旋,自旋到一定次數(shù)還沒有獲取到鎖就會(huì)進(jìn)行鎖升級(jí),因?yàn)樽孕彩钦加肅PU的,長(zhǎng)時(shí)間自旋也是很耗性能的。鎖降級(jí)因?yàn)槿绻麤]有多線程競(jìng)爭(zhēng),還是使用重量級(jí)鎖會(huì)造成額外的開銷,所以當(dāng)JVM進(jìn)入SafePoint安全點(diǎn)(可以簡(jiǎn)單的認(rèn)為安全點(diǎn)就是所有用戶線程都停止的,只有JVM垃圾回收線程可以執(zhí)行)的時(shí)候,會(huì)檢查是否有閑置的Monitor,然后試圖進(jìn)行降級(jí)。
6.偏向鎖,輕量鎖,重量鎖的適用場(chǎng)景,優(yōu)缺點(diǎn)是什么?
篇幅有限,下面是各種鎖的優(yōu)缺點(diǎn)級(jí)適用場(chǎng)景,來自《并發(fā)編程的藝術(shù)》:
參考鏈接:https://github.com/farmerjohngit/myblog/issues/12
http://redspider.group:4000/article/02/9.html
https://blog.csdn.net/bohu83/article/details/51141836
https://blog.csdn.net/Dev_Hugh/article/details/106577862