[java][鎖]java鎖的膨脹和優化

加瓦一枚發表於2019-02-22

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

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),這時要釋放鎖並且喚醒阻塞的執行緒。

相關文章