JAVA鎖的優化和膨脹過程

轉自:https://www.cnblogs.com/twoheads/p/10148598.html

首先說一下鎖的優化策略。

1,自旋鎖

自選鎖其實就是在拿鎖時發現已經有線程拿了鎖,自己如果去拿會阻塞自己,這個時候會選擇進行一次忙循環嘗試。也就是不停循環看是否能等到上個線程自己釋放鎖。這個問題是基於一個現實考量的:很多拿了鎖的線程會很快釋放鎖。因爲一般敏感的操作不會很多。當然這個是一個不能完全確定的情況,只能說總體上是一種優化。

舉個例子就好比一個人要上廁所發現廁所裏面有人,他可以:1,等一小會。2,跑去另外的地方上廁所。等一小會不一定能等到前一個人出來,不過如果跑去別的廁所的花費的時間肯定比等一小會結果前一個人出來了長。當然等完了結果那個人沒出來還是要跑去別的地方上廁所這是最慢的。

然後是基於這種做法的一個優化:自適應自旋鎖。也就是說,第一次設置最多自旋10次,結果在自旋的過程中成功獲得了鎖,那麼下一次就可以設置成最多自旋20次。道理是:一個鎖如果能夠在自旋的過程中被釋放說明很有可能下一次也會發生這種事。那麼就更要給這個鎖某種“便利”方便其不阻塞得鎖(畢竟快了很多)。同樣如果多次嘗試的結果是完全不能自旋等到其釋放鎖,那麼就說明很有可能這個臨界區裏面的操作比較耗時間。就減小自旋的次數,因爲其可能性太小了。

2,鎖粗化

試想有一個循環,循環裏面是一些敏感操作,有的人就在循環裏面寫上了synchronized關鍵字。這樣確實沒錯不過效率也許會很低,因爲其頻繁地拿鎖釋放鎖。要知道鎖的取得(假如只考慮重量級MutexLock)是需要操作系統調用的,從用戶態進入內核態,開銷很大。於是針對這種情況也許虛擬機發現了之後會適當擴大加鎖的範圍(所以叫鎖粗化)以避免頻繁的拿鎖釋放鎖的過程。

3,鎖消除

通過逃逸分析發現其實根本就沒有別的線程產生競爭的可能(別的線程沒有臨界量的引用),而“自作多情”地給自己加上了鎖。有可能虛擬機會直接去掉這個鎖。

膨脹過程

4,偏向鎖和輕量級鎖

這兩個鎖既是一種優化策略,也是一種膨脹過程所以一起說。首先它們的關係是:最高效的是偏向鎖,儘量使用偏向鎖,如果不能(發生了競爭)就膨脹爲輕量級鎖,這樣優化的效率不如原來高不過還是一種優化(對比重量級鎖而言)。所以整個過程是儘可能地優化。

首先說說偏向鎖。

HotSpot的研究人員發現大多數情況下雖然加了鎖,但是沒有競爭的發生,甚至是同一個線程反覆獲得這個鎖。那麼偏向鎖就爲了針對這種情況。

舉個例子,一個倉庫管理員管着鑰匙,然而每一次都是老王去借,倉庫管理員於是就認識了老王,直接和他說,“行,你直接拿就是不用填表格了我記得你”。

講一下偏向鎖的具體過程。首先JVM要設置爲可用偏向鎖。然後當一個進程訪問同步塊並且獲得鎖的時候,會在對象頭和棧幀的鎖記錄裏面儲存取得偏向鎖的線程ID。

下一次有線程嘗試獲取鎖的時候,首先檢查這個對象頭的MarkWord是不是儲存着這個線程的ID。如果是,那麼直接進去而不需要任何別的操作。如果不是,那麼分爲兩種情況。1,對象的偏向鎖標誌位爲0(當前不是偏向鎖),說明發生了競爭,已經膨脹爲輕量級鎖,這時使用CAS操作嘗試獲得鎖(這個操作具體是輕量級鎖的獲得鎖的過程下面講)。2,偏向鎖標誌位爲1,說明還是偏向鎖不過請求的線程不是原來那個了。這時只需要使用CAS嘗試把對象頭偏向鎖從原來那個線程指向目前求鎖的線程。這種情況舉個例子就是老王準備退休了,他兒子接替他來拿鑰匙,於是倉庫管理員認識了他兒子,他兒子每次來也不用登記註冊了。

這個CAS失敗了呢?首先必須明確這個CAS爲什麼會失敗,也就是說發生了競爭,有別的線程和它搶鎖並且搶贏了,那麼這個情況下,它就會要求撤銷偏向鎖(因爲發生了競爭)。接着它首先暫停擁有偏向鎖的線程,檢查這個線程是否是個活動線程,如果不是,那麼好,你拿了鎖但是沒在幹事,鎖還記錄着你,那麼直接把對象頭設置爲無鎖狀態重新來過。如果還是活動線程,先遍歷棧幀裏面的鎖記錄,讓這個偏向鎖變爲無鎖狀態,然後恢復線程。

再說輕量級鎖。這是偏向鎖膨脹之後的產物。

加鎖的過程:JVM在當前線程的棧幀中創建用於儲存鎖記錄的空間(LockRecord),然後把MarkWord放進去,同時生成一個叫Owner的指針指向那個加鎖的對象,同時用CAS嘗試把對象頭的MarkWord成一個指向鎖記錄的指針。成功了就拿到了鎖。那麼失敗了呢?失敗了的說法比較多。主流有《深入理解JVM》的說法和《併發編程的藝術》的說法。

《深入理解JVM》的說法:

失敗了,去查看MarkWord的值。有2種可能:1,指向當前線程的指針,2,別的值。

如果是1,那麼說明發生了“重入”的情況,直接當做成功獲得鎖處理。

其實這個有個疑問,爲什麼獲得鎖成功了而CAS失敗了,這裏其實要牽扯到CAS的具體過程:先比較某個值是不是預測的值,是的話就動用原子操作交換(或賦值),否則不操作直接返回失敗。在用CAS的時候期待的值是其原本的MarkWord。發生“重入”的時候會發現其值不是期待的原本的MarkWord,而是一個指針,所以當然就返回失敗,但是如果這個指針指向這個線程,那麼說明其實已經獲得了鎖,不過是再進入一次。如果不是這個線程,那麼情況2:

如果是2,那麼發生了競爭,鎖會膨脹爲一個重量級鎖(MutexLock)

《併發編程的藝術》的說法:

失敗了直接自旋。期望在自旋的時間內獲得鎖,如果還是不能獲得,那麼開始膨脹,修改鎖的MarkWord改爲重量級鎖的指針,並且阻塞自己。

解鎖過程:(那個拿到鎖的線程)用CAS把MarkWord換回到原來的對象頭,如果成功,那麼沒有競爭發生,解鎖完成。如果失敗,表示存在競爭(之前有線程試圖通過CAS修改MarkWord),這時要釋放鎖並且喚醒阻塞的線程。

重量級鎖

重量級鎖(heavy weight lock),是使用操作系統互斥量(mutex)來實現的傳統鎖。 當所有對鎖的優化都失效時,將退回到重量級鎖。它與輕量級鎖不同競爭的線程不再通過自旋來競爭線程, 而是直接進入堵塞狀態,此時不消耗CPU,然後等擁有鎖的線程釋放鎖後,喚醒堵塞的線程, 然後線程再次競爭鎖。但是注意,當鎖膨脹(inflate)爲重量鎖時,就不能再退回到輕量級鎖。

 

 

java中每個對象都可作爲鎖,鎖有四種級別,按照量級從輕到重分爲:無鎖、偏向鎖、輕量級鎖、重量級鎖。每個對象一開始都是無鎖的,隨着線程間爭奪鎖,越激烈,鎖的級別越高,並且鎖只能升級不能降級。

一、java對象頭

 鎖的實現機制與java對象頭息息相關,鎖的所有信息,都記錄在java的對象頭中。用2字(32位JVM中1字=32bit=4baye)存儲對象頭,如果是數組類型使用3字存儲(還需存儲數組長度)。對象頭中記錄了hash值、GC年齡、鎖的狀態、線程擁有者、類元數據的指針。

 

 

二、偏向鎖

 在實際應用運行過程中發現,“鎖總是同一個線程持有,很少發生競爭”,也就是說鎖總是被第一個佔用他的線程擁有,這個線程就是鎖的偏向線程。

 那麼只需要在鎖第一次被擁有的時候,記錄下偏向線程ID。這樣偏向線程就一直持有着鎖,直到競爭發生才釋放鎖。以後每次同步,檢查鎖的偏向線程ID與當前線程ID是否一致,如果一致直接進入同步,退出同步也,無需每次加鎖解鎖都去CAS更新對象頭,如果不一致意味着發生了競爭,鎖已經不是總是偏向於同一個線程了,這時候需要鎖膨脹爲輕量級鎖,才能保證線程間公平競爭鎖。

1.加鎖


偏向鎖加鎖發生在偏向線程第一次進入同步塊時,CAS原子操作嘗試更新對象的Mark Word(偏向鎖標誌位爲"1",記錄偏向線程的ID)。

2.撤銷偏向鎖

 當有另一個線程來競爭鎖的時候,就不能再使用偏向鎖了,要膨脹爲輕量級鎖。
競爭線程嘗試CAS更新對象頭失敗,會等待到全局安全點(此時不會執行任何代碼)撤銷偏向鎖。

 

三、輕量級鎖

 輕量鎖與偏向鎖不同的是:

  1. 輕量級鎖每次退出同步塊都需要釋放鎖,而偏向鎖是在競爭發生時才釋放鎖
  2. 每次進入退出同步塊都需要CAS更新對象頭
  3. 爭奪輕量級鎖失敗時,自旋嘗試搶佔鎖

 可以看到輕量鎖適合在競爭情況下使用,其自旋鎖可以保證響應速度快,但自旋操作會佔用CPU,所以一些計算時間長的操作不適合使用輕量級鎖。

1.加鎖

 加鎖過程和偏向鎖加鎖差不多,也是CAS修改對象頭,只是修改的內容不同。

  1. 在MarkWord中保存當前線程的指針
  2. 修改鎖標識位爲“00”

採用CAS操作的原因是,不想在加鎖解鎖上再加同步

 如果對象處於無鎖狀態(偏向鎖標誌位爲"0",鎖標誌位爲"01"),會在線程的棧中開闢個鎖記錄空間(Lock Record),將Mark Word拷貝一份到Lock Record中,稱爲Displaced Mark Word,在Lock Record中保存對象頭的指針(owner)。
接下來CAS更新MarkWord,將MarkWord指向當前線程,owner指向MarkWord,如果失敗了,則意味着出現了另一個線程競爭鎖,此時需要鎖膨脹爲輕量級鎖。

 

 

2.解鎖

 用CAS操作鎖置爲無鎖狀態(偏向鎖位爲"0",鎖標識位爲"01"),若CAS操作失敗則是出現了競爭,鎖已膨脹爲重量級鎖了,此時需要釋放鎖(持有重量級鎖線程的指針位爲"0",鎖標識位爲"10")並喚醒重量鎖的線程。

3.膨脹爲重量級鎖

 當競爭線程嘗試佔用輕量級鎖失敗多次之後,輕量級鎖就會膨脹爲重量級鎖,重量級線程指針指向競爭線程,競爭線程也會阻塞,等待輕量級線程釋放鎖後喚醒他。

 

三、重量級鎖

 重量級鎖的加鎖、解鎖過程和輕量級鎖差不多,區別是:競爭失敗後,線程阻塞,釋放鎖後,喚醒阻塞的線程,不使用自旋鎖,不會那麼消耗CPU,所以重量級鎖適合用在同步塊執行時間長的情況下。

 

偏向鎖、輕量級鎖和重量級鎖對比

鎖類型 優點 缺點 適用場景
偏向鎖 加鎖、解鎖不需要額外資源消耗,效率較高 如果線程間存在鎖競爭,會帶來額外的解鎖消耗 適用只有一個線程訪問同步塊的情景
輕量級鎖 競爭的線程不會阻塞,提高了程序響應速度 如果獲取鎖失敗,會進入自旋消耗cpu 針對鎖佔用時間短,對響應時間比較敏感的情況
重量級鎖 線程競爭不使用自旋,不消耗cpu 線程會被阻塞,影響響應時間 鎖佔用時間較長,對吞吐量要求較高
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章