1. 程式人生 > 其它 >synchronized 鎖升級狀態

synchronized 鎖升級狀態

在Java高併發系統中,我們常常需要使用多執行緒技術來提高系統的執行速度,而多執行緒帶來的資料安全問題就是我們必須要解決的問題。在Java中,可以使用synchronized關鍵字來實現多執行緒併發中的資料安全問題。

這裡簡單介紹下synchronized的三種用法:

修飾例項方法:以例項物件作為鎖,進入同步程式碼前需要獲得當前例項物件的鎖
修飾類方法(static修飾的方法):以類物件為鎖,進入同步程式碼塊前需要獲得當前類物件的鎖
修飾程式碼塊:需要指定一個鎖物件(既可以是例項物件,也可以是)
即同步方法預設用 this 或者當前類 class 物件作為鎖;同步程式碼塊可以選擇以什麼來加鎖,比同步方法要更細顆粒度,我們可以選擇只同步會發生同步問題的部分程式碼而不是整個方法。

 

2.synchronized的升級
這裡先介紹下使用者態和核心態的概念,方便大家理解。

2.1使用者態和核心態
上面提到了,重量級鎖獲取鎖和釋放鎖需要經過作業系統,這是一個重量級操作,這句話是什麼意思呢?

核心態:其實從本質上說就是我們所說的核心,它是一種特殊的軟體程式,特殊在哪兒呢?控制計算機的硬體資源,例如協調CPU資源,分配記憶體資源,並且提供穩定的環境供應用程式執行。
使用者態:使用者態就是提供應用程式執行的空間,為了使應用程式訪問到核心管理的資源例如CPU,記憶體,I/O。核心必須提供一組通用的訪問介面,這些介面就叫系統呼叫。
2.2使用者態到核心態的切換
使用者程式都是執行在使用者態的,但是有時候程式確實需要做一些核心的事情,例如從硬碟讀取資料,或者從硬盤獲取輸入,而唯一可以做這些事情的就是作業系統即核心態(synchronized中依賴的monitor也需要依賴作業系統完成,因此需要使用者態到核心態的切換)所以程式就需要先向作業系統請求以程式的名義來執行這些操作。

這時候就需要:將使用者態程式切換到核心態,但是不能控制在核心態中執行的命令 這部分先不做太多解釋,需要知道的是synchronized是依賴作業系統實現的,因此在使用synchronized同步鎖的時候需要進行使用者態到核心態的切換。

2.3synchronized 核心態切換
簡單來說在JVM中synchronized重量級鎖的底層原理monitorenter和monitorexit位元組碼依賴於底層的作業系統的Mutex Lock來實現的,但是由於使用Mutex Lock需要將當前執行緒掛起並從使用者態切換到核心態來執行,這種切換的代價是非常昂貴的。

2.3為什麼優化synchronized
在JDK1.5之前,synchronized是重量級鎖,1.6以後對其進行了優化,有了一個 無鎖-->偏向鎖-->自旋鎖-->重量級鎖 的鎖升級的過程,而不是一上來就是重量級鎖了,為什麼呢?因為重量級鎖獲取鎖和釋放鎖需要經過作業系統,是一個重量級的操作。對於重量鎖來說,一旦執行緒獲取失敗,就要陷入阻塞狀態,並且是作業系統層面的阻塞,這個過程涉及使用者態到核心態的切換,是一個開銷非常大的操作。而研究表明,執行緒持有鎖的時間是比較短暫的,也就是說,當前執行緒即使現在獲取鎖失敗,但可能很快地將來就能夠獲取到鎖,這種情況下將執行緒掛起是很不划算的行為。所以要對"synchronized總是啟用重量級鎖"這個機制進行優化。

 

3.synchronized升級原理
3.1 Java物件在記憶體中的佈局
在Java虛擬機器中,普通物件在記憶體中分為三塊區域:物件頭、例項資料、對齊填充資料,而物件頭包括markword(8位元組)和型別指標(開啟壓縮指標4位元組,不開啟8位元組,如果是32g以上記憶體,都是8位元組),例項資料就是物件的成員變數,padding就是為了保證物件的大小為8位元組的倍數,將物件所佔位元組數補到能被8整除。陣列物件比普通物件在物件頭位置多一個數組長度。

如下圖:

 

 

例如,Object o = new Object();(16g,開啟指標壓縮)在記憶體中佔了 8(markWord)+4(classPointer)+4(padding)=16位元組。

3.2 Mark Word
我們今天關注的重點是物件頭中的markWord。它裡面到底存了哪些資訊呢?

請看下圖(Hotspot 64位虛擬機器的實現):

 

 

3.3 synchronized升級
3.3.1 無鎖態
偏向鎖位、鎖標誌位的值為:0 01,此時物件是沒有做任何同步限制的,為什麼會有這個狀態在下面的偏向鎖中會跟大家介紹。

3.3.2 偏向鎖
偏向鎖位、鎖標誌位的值為:1 01。

有研究表明,其實在大部分場景都不會發生鎖資源競爭,並且鎖資源往往都是由一個執行緒獲得的。如果這種情況下,同一個執行緒獲取這個鎖都需要進行一系列操作,比如說CAS自旋,那這個操作很明顯是多餘的。偏向鎖就解決了這個問題。其核心思想就是:一個執行緒獲取到了鎖,那麼鎖就會進入偏向模式,當同一個執行緒再次請求該鎖的時候,無需做任何同步,直接進行同步區域執行。這樣就省去了大量有關鎖申請的操作。所以,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果。

偏向鎖加鎖過程:

 

 

訪問Mark Word中偏向鎖的標識是否設定成1,鎖標誌位是否為01,確認為可偏向狀態。
如果為可偏向狀態,則判斷執行緒ID是否指向當前執行緒,如果是,進入步驟5,否則進入步驟3。
如果執行緒ID並未指向當前執行緒,則通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中執行緒ID設定為當前執行緒ID,然後執行5;如果競爭失敗,執行4。
如果CAS獲取偏向鎖失敗,則表示有競爭。當到達全域性安全點(safepoint)時獲得偏向鎖的執行緒被掛起,偏向鎖升級為輕量級鎖,然後被阻塞在安全點的執行緒繼續往下執行同步程式碼。
執行同步程式碼。
偏向鎖的撤銷在上述第四步驟中有提到。偏向鎖只有遇到其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖,執行緒不會主動去釋放偏向鎖。偏向鎖的撤銷,需要等待全域性安全點(在這個時間點上沒有位元組碼正在執行),它會首先暫停擁有偏向鎖的執行緒,判斷鎖物件是否處於被鎖定狀態,撤銷偏向鎖後恢復到未鎖定(標誌位為“01”)或輕量級鎖(標誌位為“00”)的狀態。

偏向鎖的適用場景

始終只有一個執行緒在執行同步塊,在它沒有執行完釋放鎖之前,沒有其它執行緒去執行同步塊,在鎖無競爭的情況下使用,一旦有了競爭就升級為輕量級鎖,升級為輕量級鎖的時候需要撤銷偏向鎖。在有鎖的競爭時,偏向鎖會多做很多額外操作,尤其是撤銷偏向鎖的時候會導致進入安全點,安全點會導致stw,導致效能下降,這種情況下應當禁用。所以一般JVM並不是一開始就開啟偏向鎖的,而是有一定的延遲,這也就是為什麼會有無鎖態的原因。可以使用-XX:BiasedLockingStartupDelay=0來關閉偏向鎖的啟動延遲, 也可以使用-XX:-UseBiasedLocking=false來關閉偏向鎖。偏向鎖撤銷導致的stw

通過加偏向鎖的方式可以看到,物件中記錄了獲取到物件鎖的執行緒ID,這就意味如果短時間同一個執行緒再次訪問這個加鎖的同步程式碼或方法時,該執行緒只需要對物件頭Mark Word中去判斷一下是否有偏向鎖指向它的ID,有的話就繼續執行邏輯了,沒有的話,會CAS嘗試獲得鎖,如果持有鎖的執行緒在全域性安全點檢查時,不需要再使用該鎖了則獲取成功,程式繼續執行,反之則獲取鎖失敗,撤銷偏向狀態,升級為輕量級鎖,即自旋鎖。

3.3.3 輕量級鎖(自旋鎖)

當有另外一個執行緒競爭獲取這個鎖時,由於該鎖已經是偏向鎖,當發現物件頭 Mark Word 中的執行緒 ID 不是自己的執行緒 ID,銷偏向鎖狀態,將鎖物件markWord中62位修改成指向自己執行緒棧中Lock Record的指標(CAS搶)執行在使用者態,消耗CPU的資源(自旋鎖不適合鎖定時間長的場景、等待執行緒特別多的場景),此時鎖標誌位為:00。

自旋策略
JVM 提供了一種自旋鎖,可以通過自旋方式不斷嘗試獲取鎖,從而避免執行緒被掛起阻塞。這是基於大多數情況下,執行緒持有鎖的時間都不會太長,畢竟執行緒被掛起阻塞可能會得不償失。

自適應自旋鎖

JDK 1.6引入了更加聰明的自旋鎖,叫做自適應自旋鎖。他的自旋次數是會變的,我用大白話來講一下,就是執行緒如果上次自旋成功了,那麼這次自旋的次數會更加多,因為虛擬機器認為既然上次成功了,那麼這次自旋也很有可能會再次成功。反之,如果某個鎖很少有自旋成功,那麼以後的自旋的次數會減少甚至省略掉自旋過程,以免浪費處理器資源。 大家現在覺得沒這麼low了吧。

輕量級鎖的加鎖過程:

在程式碼進入同步塊的時候,如果同步物件鎖狀態為無鎖狀態(鎖標誌位為“01”狀態,是否為偏向鎖為“0”),虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word。
拷貝物件頭中的Mark Word複製到鎖記錄中;
拷貝成功後,虛擬機器將使用CAS操作嘗試將物件的Mark Word中的62位更新為指向Lock Record的指標,並將Lock record裡的owner指標指向object mark word。如果更新成功,則執行步驟4,否則執行步驟5。
如果這個更新動作成功了,那麼這個執行緒就擁有了該物件的鎖,並且物件Mark Word的鎖標誌位設定為“00”,即表示此物件處於輕量級鎖定狀態。
如果這個更新操作失敗了,虛擬機器首先會檢查物件的Mark Word是否指向當前執行緒的棧幀,如果是就說明當前執行緒已經擁有了這個物件的鎖,那就可以直接進入同步塊繼續執行。此時為了提高獲取鎖的效率,執行緒會不斷地迴圈去獲取鎖, 這個迴圈是有次數限制的, 如果在迴圈結束之前CAS操作成功, 那麼執行緒就獲取到鎖, 如果迴圈結束依然獲取不到鎖, 則獲取鎖失敗, 物件的MarkWord中的記錄會被修改為指向互斥量(重量級鎖)的指標,鎖標誌的狀態值變為10,執行緒被掛起,後面來的執行緒也會直接被掛起。
輕量級鎖的釋放

釋放鎖執行緒視角:由輕量鎖切換到重量鎖,是發生在輕量鎖釋放鎖的期間,之前在獲取鎖的時候它拷貝了鎖物件頭的markword,在釋放鎖的時候如果它發現在它持有鎖的期間有其他執行緒來嘗試獲取鎖了,並且該執行緒對markword做了修改,兩者比對發現不一致,則切換到重量鎖。因為重量級鎖被修改了,所有display mark word和原來的markword不一樣了。
怎麼補救,就是進入mutex前,compare一下obj的markword狀態。確認該markword是否被其他執行緒持有。此時如果執行緒已經釋放了markword,那麼通過CAS後就可以直接進入執行緒,無需進入mutex,就這個作用。

嘗試獲取鎖執行緒視角:如果執行緒嘗試獲取鎖的時候,輕量鎖正被其他執行緒佔有,那麼它就會修改markword,修改重量級鎖,表示該進入重量鎖了。

從 JDK1.7 開始,自旋鎖預設啟用,自旋次數由 JVM 設定決定,這裡我不建議設定的重試次數過多,因為 CAS 重試操作意味著長時間地佔用 CPU。自旋鎖重試之後如果搶鎖依然失敗,同步鎖就會升級至重量級鎖,鎖標誌位改為 10。在這個狀態下,未搶到鎖的執行緒都會進入 Monitor,之後會被阻塞在 _WaitSet 佇列中。

3.3.4 重量級鎖

此時鎖標誌位為:10。前面我們提到的markWord,若是重量鎖,物件頭中還會存在一個監視器物件,也就是Monitor物件。這個Monitor物件就是實現synchronized的一個關鍵。

在Java虛擬機器(HotSpot)中,Monitor物件其實就是ObjectMonitor物件,這個物件是一個C++物件,定義在虛擬機器原始碼中。

ObjectMonitor有比較多的屬性,但是比較重要的屬性有四個:

_count:計數器。用來記錄獲取鎖的次數。該屬性主要用來實現重入鎖機制。
_owner:記錄著當前鎖物件的持有者執行緒。
_WaitSet:佇列。當一個執行緒呼叫了wait方法後,它會釋放鎖資源,進入WaitSet佇列等待被喚醒。
_EntryList:佇列。裡面存放著所有申請該鎖物件的執行緒。
所以一個執行緒獲取鎖物件的流程如下:

判斷鎖物件的鎖標誌位是重量級鎖,於是想要獲取Monitor物件鎖。
如果Monitor中的_count屬性是0,說明當前鎖可用,於是把 _owner 屬性設定為本執行緒,然後把 _count 屬性+1。這就成功地完成了鎖的獲取。
如果Monitor中的_count屬性不為0,再檢查 _owner 屬性,如果該屬性指向了本執行緒,說明可以重入鎖,於是把 _count 屬性再加上1,實現鎖的衝入。
如果 _owner 屬性指向了其他執行緒,那麼該執行緒進入 _EntryList 佇列中等待鎖資源的釋放。
如果執行緒在持有鎖的過程中呼叫了wait()方法,那麼執行緒釋放鎖物件,然後進入 _WaitSet 佇列中等待被喚醒。
4.synchronized可重入
synchronized是可重入鎖,那麼它是如何實現可重入的呢?其實上面詳細的過程已經說過了,這裡再總結一下(之前的判斷邏輯就省略掉了):

偏向鎖:檢查markWord中的執行緒ID是否是當前執行緒,如果是的話就獲取鎖,繼續執行程式碼;
輕量級鎖:檢查markWord中指向lockRecord的指標是否是指向當前執行緒的lockRecord,是的話繼續執行程式碼;
重量級鎖:檢查_owner屬性,如果該屬性指向了本執行緒,_count屬性+1,並繼續執行程式碼。
5.總結
synchronized的執行過程:
1. 檢測Mark Word裡面是不是當前執行緒的ID,如果是,表示當前執行緒處於偏向鎖
2. 如果不是,則使用CAS將當前執行緒的ID替換Mard Word,如果成功則表示當前執行緒獲得偏向鎖,置偏向標誌位1
3. 如果失敗,則說明發生競爭,撤銷偏向鎖,進而升級為輕量級鎖。
4. 當前執行緒使用CAS將物件頭的Mark Word替換為鎖記錄指標,如果成功,當前執行緒獲得鎖
5. 如果失敗,表示其他執行緒競爭鎖,當前執行緒便嘗試使用自旋來獲取鎖。
6. 如果自旋成功則依然處於輕量級狀態。
7. 如果自旋失敗,則升級為重量級鎖。

上面幾種鎖都是JVM自己內部實現,當我們執行synchronized同步塊的時候jvm會根據啟用的鎖和當前執行緒的爭用情況,決定如何執行同步操作;

在所有的鎖都啟用的情況下執行緒進入臨界區時會先去獲取偏向鎖,如果已經存在偏向鎖了,則會嘗試獲取輕量級鎖,啟用自旋鎖,如果自旋也沒有獲取到鎖,則使用重量級鎖,沒有獲取到鎖的執行緒阻塞掛起,直到持有鎖的執行緒執行完同步塊喚醒他們;

synchronized鎖升級實際上是把本來的悲觀鎖變成了 在一定條件下 使用無鎖(同樣執行緒獲取相同資源的偏向鎖),以及使用樂觀(自旋鎖 cas)和一定條件下悲觀(重量級鎖)的形式。

偏向鎖:適用於單執行緒適用鎖的情況,如果執行緒爭用激烈,那麼應該禁用偏向鎖。

輕量級鎖:適用於競爭較不激烈的情況(這和樂觀鎖的使用範圍類似)

重量級鎖:適用於競爭激烈的情況

6.鎖優化
以上介紹的鎖不是我們程式碼中能夠控制的,但是借鑑上面的思想,我們可以優化我們自己執行緒的加鎖操作;

6.1鎖消除
鎖消除用大白話來講,就是在一段程式裡你用了鎖,但是jvm檢測到這段程式裡不存在共享資料競爭問題,也就是變數沒有逃逸出方法外,這個時候jvm就會把這個鎖消除掉

我們程式設計師寫程式碼的時候自然是知道哪裡需要上鎖,哪裡不需要,但是有時候我們雖然沒有顯示使用鎖,但是我們不小心使了一些執行緒安全的API時,如StringBuffer、Vector、HashTable等,這個時候會隱形的加鎖。比如下段程式碼:

public void sbTest(){
StringBuffer sb= new StringBuffer();
for(int i = 0 ; i < 10 ; i++){
sb.append(i);
}

System.out.println(sb.toString());
}
上面這段程式碼,JVM可以明顯檢測到變數sb沒有逃逸出方法sbTest()之外,所以JVM可以大膽地將sbTest內部的加鎖操作消除。

6.2 減少鎖的時間
不需要同步執行的程式碼,能不放在同步快裡面執行就不要放在同步快內,可以讓鎖儘快釋放;

6.3減小鎖的粒度
它的思想是將物理上的一個鎖,拆成邏輯上的多個鎖,增加並行度,從而降低鎖競爭。它的思想也是用空間來換時間(如ConcurrentHashMap、LinkedBlockingQueue、LongAdder);

6.4鎖粗化
大部分情況下我們是要讓鎖的粒度最小化,鎖的粗化則是要增大鎖的粒度; 在以下場景下需要粗化鎖的粒度:
假如有一個迴圈,迴圈內的操作需要加鎖,我們應該把鎖放到迴圈外面,否則每次進出迴圈,都進出一次臨界區,效率是非常差的;

6.5使用讀寫鎖
ReentrantReadWriteLock 是一個讀寫鎖,讀操作加讀鎖,可以併發讀,寫操作使用寫鎖,只能單執行緒寫。

參考文章:

https://zhuanlan.zhihu.com/p/69554144

https://www.cnblogs.com/linghu-java/p/8944784.html

偏向鎖撤銷導致stw

https://blog.csdn.net/hnjsjsac/article/details/105778846?utm_medium=distribute.pc_relevant_t0.none-task-blog-OPENSEARCH-1.compare&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-OPENSEARCH-1.compare
————————————————
版權宣告:本文為CSDN博主「CrazySnail_x」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處連結及本宣告。
原文連結:https://blog.csdn.net/weixin_40910372/article/details/107726978