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