面試官超級(jí)喜歡問(wèn)的 Synchronized 鎖
前言
最近技術(shù)圈子里因log4j的漏洞炸開了鍋。
Synchronized鎖在面試當(dāng)中難免會(huì)遇到,那么如何完美應(yīng)對(duì)面試官角度刁鉆的問(wèn)題就顯得尤為重要。阿巴阿巴以身作則,給大家貢獻(xiàn)面試經(jīng)驗(yàn)。
回家等通知
面試官: synchronized應(yīng)該了解吧?講講。
阿巴阿巴: 嗯嗯,了解一些,synchronized是Java中的關(guān)鍵字,它的作用主要是用來(lái)同步,一般叫它同步鎖,一般可以用在方法上,以及代碼塊上。
阿巴阿巴: 用在方法上好像鎖的的對(duì)象,用在代碼塊上如果修飾的是對(duì)象則鎖的是對(duì)象,如果修飾的是類,那么鎖的是該類的所有對(duì)象。
面試官: 不錯(cuò),那synchronized可以用在構(gòu)造方法上嗎?上鎖的過(guò)程你了解嗎?
阿巴阿巴: 嗯...這個(gè)...不太清楚。
面試官: 那可以講一下鎖的優(yōu)化嗎?
阿巴阿巴: 嗯?鎖還有優(yōu)化嗎?不是很清楚哦。
面試官: 好的,那今天先面到這里吧,你回去等我通知哈??
阿巴阿巴: 好的。
當(dāng)場(chǎng)發(fā)offer
面試官: synchronized應(yīng)該了解吧?講講。
阿巴阿巴: 嗯嗯,了解一些,synchronized是Java中的關(guān)鍵字,它的作用主要是用來(lái)同步,一般叫它同步鎖,一般可以用在實(shí)例方法上、靜態(tài)方法上以及代碼塊上,主要是維護(hù)一個(gè)狀態(tài),這個(gè)狀態(tài)就是同一時(shí)刻,只能有一個(gè)線程去訪問(wèn)synchronized修飾的方法或代碼塊。
阿巴阿巴: 用在實(shí)例方法上鎖的是調(diào)用該方法的對(duì)象,用在靜態(tài)方法上,鎖的是當(dāng)前類的所有對(duì)象,用在代碼塊上如果修飾的是對(duì)象則鎖的是對(duì)象,如果修飾的是類,那么鎖的是該類的所有對(duì)象。(畫圖強(qiáng)化記憶)
- // synchronized用在靜態(tài)方法上
- public synchronized static void test01() {
- }
- // synchronized用在實(shí)例方法上
- public synchronized void test02() {
- }
- // synchronized用來(lái)修飾對(duì)象
- public void test03() {
- synchronized (this) {}
- }
- // synchronized用來(lái)修飾當(dāng)前類
- public void test04() {
- synchronized (TestSyn.class) {}
- }
面試官: 不錯(cuò),那synchronized可以用在構(gòu)造方法上嗎?上鎖的過(guò)程你了解嗎?
阿巴阿巴: synchronized不能直接加在構(gòu)造方法上,但是可以在構(gòu)造方法里使用synchronized的代碼塊。
阿巴阿巴: 上鎖過(guò)程這里涉及到JDK版本問(wèn)題,在JDK1.5及之前的話,synchronized關(guān)鍵字經(jīng)過(guò)編譯之后,會(huì)在同步塊的前后分別形成monitorenter和monitorexit這倆個(gè)字節(jié)碼指令,在執(zhí)行monitorenter指令的時(shí)候?qū)ο箧i(這個(gè)對(duì)象鎖包括對(duì)象實(shí)例或Class對(duì)象),如果說(shuō)獲取的這個(gè)對(duì)象沒有被鎖定,或者說(shuō)當(dāng)前線程已經(jīng)獲取到該對(duì)象的鎖了(synchronized是可重入鎖,即已經(jīng)獲取到鎖的線程可以再次獲取鎖,而不需要再進(jìn)行同步),那么就把鎖的計(jì)數(shù)器加1,
阿巴阿巴: 同樣,如果在執(zhí)行monitorexit這個(gè)指令時(shí),就把鎖的計(jì)數(shù)器減1,這樣當(dāng)計(jì)數(shù)器的值為0時(shí),鎖就被釋放了。倘若線程獲取對(duì)象鎖沒成功,那么就會(huì)一直阻塞等待直到鎖被釋放。
阿巴阿巴: synchronized重量級(jí)鎖的實(shí)現(xiàn)是由C++代碼實(shí)現(xiàn)的,其中有個(gè)ObjectMonitor隊(duì)列,下面展示下代碼中的重要屬性。
- ObjectMonitor() {
- _recursions = 0; //重入次數(shù)
- _owner = NULL; //指向持有ObjectMonitor對(duì)象的線程
- _WaitSet = NULL; //調(diào)用wait后,線程會(huì)被加入到_WaitSet,WaitSet是第一個(gè)節(jié)點(diǎn)
- _cxq = NULL ; //多線程競(jìng)爭(zhēng)鎖進(jìn)入時(shí)的單向鏈表
- _EntryList = NULL ; //等待獲取鎖的線程,會(huì)被加入到該列表,_EntryList是第一個(gè)節(jié)點(diǎn)
- }
阿巴阿巴: 下面是線程流動(dòng)圖。
如上圖所示
0 當(dāng)多個(gè)線程同時(shí)競(jìng)爭(zhēng)時(shí),那么這些線程會(huì)被放入到EntryList隊(duì)列,此時(shí)線程處于阻塞狀態(tài)
1 當(dāng)一個(gè)線程獲取到了對(duì)象的monitor后,那么就可以進(jìn)入運(yùn)行狀態(tài),這時(shí)候ObjectMonitor對(duì)象的/_owner指向當(dāng)前線程,_count加1表示當(dāng)前對(duì)象鎖被一個(gè)線程獲取。
2 當(dāng)運(yùn)行狀態(tài)的線程調(diào)用wait()方法,那么當(dāng)前線程釋放monitor對(duì)象,進(jìn)入等待狀態(tài),ObjectMonitor對(duì)象的/_owner變?yōu)閚ull,_count減1,同時(shí)線程進(jìn)入_WaitSet隊(duì)列。
3 直到有線程調(diào)用notify()方法喚醒該線程,則該線程進(jìn)入_EntryList隊(duì)列,競(jìng)爭(zhēng)到鎖再進(jìn)入_Owner區(qū)。
4 如果當(dāng)前線程執(zhí)行完畢,那么也釋放monitor對(duì)象,ObjectMonitor對(duì)象的/_owner變?yōu)閚ull,_count減1。
阿巴阿巴: 而JDK 1.5版本之前的synchronized,每次加鎖都需要從用戶態(tài)(運(yùn)行用戶程序)切換到內(nèi)核態(tài)(運(yùn)行操作系統(tǒng)程序、操作硬件等),這種切換對(duì)系統(tǒng)資源的消耗是巨大的,因此JDK 1.6版本對(duì)synchronized進(jìn)行了優(yōu)化,引入了下面這些概念
- 自旋鎖
- 自適應(yīng)性自旋
- 鎖消除
- 鎖粗化
- 偏向鎖
- 輕量級(jí)鎖
- 重量鎖
面試官: 愿聞其詳
阿巴阿巴: 自旋鎖的引入主要是因?yàn)榇蠖鄶?shù)情況下,一個(gè)線程占用鎖的時(shí)間不會(huì)持續(xù)很長(zhǎng)時(shí)間,如果有其他線程競(jìng)爭(zhēng),直接將競(jìng)爭(zhēng)失敗的線程掛起再恢復(fù),顯然這種消耗是巨大的,所以采用一種“觀望”的手段,即讓該線程稍做等待,看看這段時(shí)間內(nèi)占有鎖的線程是否會(huì)釋放鎖,這就是自旋。
阿巴阿巴: 然而,自旋也沒能徹底解決該問(wèn)題,需要考慮到占有鎖的線程對(duì)鎖的占用,如果占用過(guò)久那么就會(huì)導(dǎo)致自旋鎖一直做無(wú)用的自選操作,從而消耗CPU資源,因此設(shè)置一個(gè)自旋的次數(shù)閾值顯得尤為重要,這個(gè)閾值也需要設(shè)置成合適的值,不會(huì)過(guò)高也不會(huì)過(guò)低。
阿巴阿巴: 自適應(yīng)自旋鎖的誕生。自適應(yīng)的意思就是說(shuō)自旋的次數(shù)或者時(shí)間不再固定了,而是由前一次在同一個(gè)鎖上的自旋的次數(shù)或者時(shí)間來(lái)決定:如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲得過(guò)鎖,并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也很有可能再次成功,M某種意義上來(lái)說(shuō)它將允許自旋等待持續(xù)相對(duì)更長(zhǎng)的時(shí)間。相反的,如果自旋很少成功獲得過(guò),那在以后要獲取這個(gè)鎖時(shí)將可能減少自旋時(shí)間、或次數(shù),從而來(lái)避免浪費(fèi)CPU資源。
阿巴阿巴: 鎖消除即在同步的代碼中分析發(fā)現(xiàn)無(wú)論如何都不會(huì)出現(xiàn)鎖的競(jìng)爭(zhēng),那么就可以將該鎖進(jìn)行消除,這個(gè)分析被稱為逃逸分析,如果有一個(gè)段同步的代碼不會(huì)被其他線程所訪問(wèn)到,那么這個(gè)同步也是無(wú)意義的。
阿巴阿巴: 鎖的粗化指的是如果一段代碼一直在不停的給一個(gè)對(duì)象進(jìn)行加鎖、解鎖,比如在循環(huán)體中進(jìn)行加鎖、解鎖操作,就算沒有線程競(jìng)爭(zhēng),也會(huì)產(chǎn)生巨大消耗的,對(duì)于這種情況可以考慮將鎖的范圍擴(kuò)大,這個(gè)過(guò)程就是粗化。
阿巴阿巴: 偏向鎖在保證線程安全的情況下,其實(shí)不一定會(huì)有線程的競(jìng)爭(zhēng),也就是不一定會(huì)有互斥,如果一個(gè)鎖對(duì)象沒有其他沒有其他線程競(jìng)爭(zhēng),那么JVM會(huì)默認(rèn)其為偏向鎖,偏向鎖默認(rèn)只有第一個(gè)申請(qǐng)鎖的線程會(huì)使用鎖且不會(huì)有其他線程來(lái)競(jìng)爭(zhēng)鎖,因此,只需要在Mark Word中CAS記錄owner,如果記錄更新成功,則偏向鎖獲取成功,記錄鎖狀態(tài)為偏向鎖,以后當(dāng)前線程等于owner就可以零成本的直接獲得鎖;如果這時(shí)候有其他線程競(jìng)爭(zhēng),那么偏向鎖就會(huì)膨脹為輕量級(jí)鎖。
阿巴阿巴: 使用輕量級(jí)鎖時(shí),不需要申請(qǐng)互斥量,僅僅將Mark Word中的部分字節(jié)CAS更新指向線程棧中的Lock Record,如果更新成功,則輕量級(jí)鎖獲取成功,記錄鎖狀態(tài)為輕量級(jí)鎖;輕量鎖適合于倆個(gè)線程交替運(yùn)行,但是沒有產(chǎn)生實(shí)質(zhì)上得競(jìng)爭(zhēng),如果發(fā)生了鎖競(jìng)爭(zhēng),接下來(lái)輕量鎖將膨脹為重量級(jí)鎖。
面試官: 講的很好,不錯(cuò),可以回去準(zhǔn)備后面的面試了??。
阿巴阿巴: 好的。
本期面試到此結(jié)束,下期阿巴阿巴被問(wèn)到了更難的對(duì)象頭和鎖相關(guān)的東西,期待她完美的表現(xiàn)吧!