速記:偏向-輕量-重量
鎖膨脹
上面講到鎖有四種狀態(tài),并且會因?qū)嶋H情況進行膨脹升級,其膨脹方向是:無鎖——>偏向鎖——>輕量級鎖——>重量級鎖,并且膨脹方向不可逆
一.鎖升級理論.
在synchronized鎖升級過程中涉及到以下幾種鎖.先說一下這幾種鎖是什么意思.
偏向鎖:只有一個線程爭搶鎖資源的時候.將線程擁有者標識為當前線程.
輕量級鎖(自旋鎖):一個或多個線程通過CAS去爭搶鎖,如果搶不到則一直自旋.
重量級鎖:多個線程爭搶鎖,向內(nèi)核申請鎖資源,將未爭搶成功的鎖放到隊列中直接阻塞.
為什么要有鎖的升級過程?
? ? ? 在最開始的時候,其實就是無鎖直接到重量級鎖,但是重量級鎖需要向內(nèi)核申請額外的鎖資源,這就涉及到用戶態(tài)和內(nèi)核態(tài)的轉(zhuǎn)換,比較浪費資源,而且大多數(shù)情況下,其實還是一個線程去爭搶鎖,完全不需要重量級鎖.
鎖的具體升級過程(通常情況下):
1.當只有一個線程去爭搶鎖的時候,會先使用偏向鎖,就是給一個標識,說明現(xiàn)在這個鎖被線程a占有.
2.后來又來了線程b,線程c,說憑什么你占有鎖,需要公平的競爭,于是將標識去掉,也就是撤銷偏向鎖
3.升級為輕量級鎖,三個線程通過CAS進行鎖的爭搶(其實這個搶鎖過程還是偏向于原來的持有偏向鎖的線程).現(xiàn)在線程a占有了鎖,線程b,線程c一直在循環(huán)嘗試獲取鎖,后來又來了十個線程,一直在自旋,那這樣等著也是干耗費CPU資源,所以就將鎖升級為重量級鎖,向內(nèi)核申請資源,直接將等待的線程進行阻塞.
鎖升級的過程如下所示:
什么情況下偏向鎖才會升級為輕量級鎖,什么時候輕量級鎖才會升級為重量級鎖?
只有一個線程的時候就是偏向鎖(當偏向鎖開啟的時候,偏向鎖默認開啟),當爭搶的線程超過一個,升級為輕量級鎖.
當自旋的線程循環(huán)超過10次,或者線程等待的數(shù)量超過cpu的1/2,升級為重量級鎖.其實輕量級鎖就適用于那種執(zhí)行任務很短的線程,可能通過一兩次自旋,就能夠獲取到鎖.
開啟偏向鎖一定比輕量級鎖高效嗎?
? ? ? 不一定,比如在一開始已經(jīng)知道某個資源就需要被多個線程爭搶,此時就不需要開啟偏向鎖,因為偏向鎖給了標識之后,還需要取消這個標識,重新?lián)屾i,比如在JVM中,偏向鎖默認是延遲4秒才開始的,因為JVM在啟動的時候需要多個線程競爭資源,并且這個都是一開始知道的.
?
對象在內(nèi)存中的內(nèi)存布局
在堆中,一個對象會包含以下四個部分:
?
- 實例數(shù)據(jù):存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息;
- 對齊填充:由于虛擬機要求?對象起始地址必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對齊;
- 對象頭:Java對象頭一般占有2個機器碼(在32位虛擬機中,1個機器碼等于4字節(jié),也就是32bit,在64位虛擬機中,1個機器碼是8個字節(jié),也就是64bit),但是?如果對象是數(shù)組類型,則需要3個機器碼,因為JVM虛擬機可以通過Java對象的元數(shù)據(jù)信息確定Java對象的大小,但是無法從數(shù)組的元數(shù)據(jù)來確認數(shù)組的大小,所以用一塊來記錄數(shù)組長度。
Synchronized用的鎖就是存在Java對象頭里的
我們?nèi)绻褂胹ynchronized對某個對象進行加鎖,就會體現(xiàn)在mark word區(qū)域.在最低兩個字節(jié)加以標識.
如下如所示:
下圖是Java對象頭?無鎖狀態(tài)下Mark Word部分的存儲結(jié)構(gòu)(32位虛擬機): 25+4+1+2=32
Mark Word存儲結(jié)構(gòu)?
對象頭信息是與對象自身定義的數(shù)據(jù)無關(guān)的額外存儲成本,但是考慮到虛擬機的空間效率,Mark Word被設(shè)計成一個非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲盡量多的數(shù)據(jù),它會根據(jù)對象的狀態(tài)復用自己的存儲空間,也就是說,Mark Word會隨著程序的運行發(fā)生變化,可能變化為存儲以下4種數(shù)據(jù):
輕量級鎖 對應 00
重量級鎖 對應 10
無鎖和偏向鎖都對應01,這個時候需要倒數(shù)第三個字節(jié)加以區(qū)分,即 無鎖 對應 001, 偏向鎖 對應 101
Mark Word可能存儲4種數(shù)據(jù)
在64位虛擬機下,Mark Word是64bit大小的,其存儲結(jié)構(gòu)如下:
64位Mark Word存儲結(jié)構(gòu)
對象頭的最后兩位存儲了鎖的標志位,01是初始狀態(tài),未加鎖,其對象頭里存儲的是對象本身的哈希碼,隨著鎖級別的不同,對象頭里會存儲不同的內(nèi)容。偏向鎖存儲的是當前占用此對象的線程ID;而輕量級則存儲指向線程棧中鎖記錄的指針。從這里我們可以看到,“鎖”這個東西,可能是個鎖記錄+對象頭里的引用指針(判斷線程是否擁有鎖時將線程的鎖記錄地址和對象頭里的指針地址比較),也可能是對象頭里的線程ID(判斷線程是否擁有鎖時將線程的ID和對象頭里存儲的線程ID比較)。?
對象頭中Mark Word與線程中Lock Record
在線程進入同步代碼塊的時候,如果此同步對象沒有被鎖定,即它的鎖標志位是01,則虛擬機首先在當前線程的棧中創(chuàng)建我們稱之為“鎖記錄(Lock Record)”的空間,用于存儲鎖對象的Mark Word的拷貝,官方把這個拷貝稱為Displaced Mark Word。整個Mark Word及其拷貝至關(guān)重要。
Lock Record是線程私有的數(shù)據(jù)結(jié)構(gòu),每一個線程都有一個可用Lock Record列表,同時還有一個全局的可用列表。每一個被鎖住的對象Mark Word都會和一個Lock Record關(guān)聯(lián)(對象頭的MarkWord中的Lock Word指向Lock Record的起始地址),同時Lock Record中有一個Owner字段存放擁有該鎖的線程的唯一標識(或者object mark word
),表示該鎖被這個線程占用。如下圖所示為Lock Record的內(nèi)部結(jié)構(gòu):
Lock Record | 描述 |
---|---|
Owner | 初始時為NULL表示當前沒有任何線程擁有該monitor record,當線程成功擁有該鎖后保存線程唯一標識,當鎖被釋放時又設(shè)置為NULL; |
EntryQ | 關(guān)聯(lián)一個系統(tǒng)互斥鎖(semaphore),阻塞所有試圖鎖住monitor record失敗的線程; |
RcThis | 表示blocked或waiting在該monitor record上的所有線程的個數(shù); |
Nest | 用來實現(xiàn)?重入鎖的計數(shù); |
HashCode | 保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。 |
Candidate | 用來避免不必要的阻塞或等待線程喚醒,因為每一次只有一個線程能夠成功擁有鎖,如果每次前一個釋放鎖的線程喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒然后因為競爭鎖失敗又被阻塞)從而導致性能嚴重下降。Candidate只有兩種可能的值0表示沒有需要喚醒的線程1表示要喚醒一個繼任線程來競爭鎖。 |
其他注意點:
??????并不是一定會有一個偏向鎖->輕量級鎖->重量級鎖的過程,比如如果出現(xiàn)嚴重的耗時操作(sleep,或者wait等),就會直接由偏向鎖升級為重量級鎖.
知識來源:
【并發(fā)與線程】Sychronized的偏向鎖、輕量級鎖、重量級鎖_嗶哩嗶哩_bilibili
【2023年多線程面試】無鎖、偏向鎖、輕量級鎖、重量級鎖升級過程(多線程面試)_嗶哩嗶哩_bilibili
?synchronized鎖升級過程_程序員bling的博客-CSDN博客文章來源:http://www.zghlxwxcb.cn/news/detail-683940.html
https://www.cnblogs.com/aspirant/p/11470858.html文章來源地址http://www.zghlxwxcb.cn/news/detail-683940.html
到了這里,關(guān)于java八股文面試[多線程]——synchronized鎖升級過程的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!