Java基礎-Synchronized原理

Merbng發表於2018-05-15

在多執行緒併發程式設計中Synchronized一直是元老級角色,很多人都會稱呼它為重量級鎖,但是隨著Java SE1.6中為了減少獲得鎖和釋放鎖帶來的效能消耗而引入的偏向鎖和輕量級鎖,以及鎖的儲存結構和升級過程。 CAS(Compare and Swap),用於在硬體層面上提供原子性操作,在Intel處理器中,比較並交換通過指令cmpxchg實現。比較是否和給定的數值一致,如果一致則修改,不一致則不修改

基礎

Java中的每一個物件都可以作為鎖。

  • 對於同步方法,鎖是當前例項物件。
  • 對於靜態同步方法,鎖是當前物件的Class物件。
  • 對於同步方法塊,鎖是Synchronized括號裡配置的物件。 當一個執行緒檢視訪問同步程式碼塊時,它首先必須得到鎖,退出或排除異常時必須釋放鎖,那麼鎖在哪裡呢?鎖裡面會儲存什麼資訊呢?

同步的原理

JVM規範規定JVM基於進入和退出Monitor物件來實現方法同步和程式碼塊同步,但是兩者的實現細節不一樣。程式碼塊同步是使用monitorenter和monitorexit指令實現,而方法同步是使用另外一種方式實現的,細節在JVM規範裡並沒有詳細說明。但是方法的同步同樣可以使用這兩個指令來實現。 monitorenter指令是在編譯後插入到同步程式碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何物件都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態。執行緒執行到monitorenter指令時,將會嘗試獲取物件所對應的monitor的所有權,即嘗試獲得物件的鎖。

Java物件頭

鎖存在Java物件頭裡,如果物件是陣列型別,則虛擬機器用3個Word(字寬)儲存物件頭,如果物件是非陣列型別,則用2字寬儲存物件頭,在32位虛擬機器中,一字寬等於四位元組,即32bit

鎖的升級

Java SE1.6 為了減少獲得鎖和釋放鎖鎖帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java SE1.6裡鎖一共有四種狀態,無鎖狀態、偏向鎖狀態、輕量級鎖狀態、和重量級鎖狀態,它會隨著競爭情況逐漸升級,鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖,這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。

執行緒對鎖的競爭

當多個執行緒同時請求某個物件監視器時,物件監視器會設定幾種狀態來區分請求的執行緒:

  • Contention List: 所有請求鎖的執行緒將會被首先放置到該競爭佇列。
  • Entry List: Contention List中那些有資格成為候選人的執行緒被移到Entry List
  • Wait Set: 那些呼叫wait方法被阻塞的執行緒被放置到Wait Set
  • OnDeck : 任何時刻最多隻能有一個執行緒正在競爭鎖,該執行緒稱為OnDeck
  • Owner: 獲得鎖的執行緒稱為Owner
  • !Owner: 釋放鎖的執行緒
    synchronized.gif

ContentionList

ContentionList並不是一個真正的Queue,而只是一個虛擬佇列,原因在於ContentionList是由Node極其next指標邏輯構成,並不存在一個Queue的資料結構。ContentionList是一個後進先出(LIFO)的佇列,每次新加入Node時都會在隊頭進行,通過CAS改變第一個節點的指標為新增節點,同時設定新增節點的next指向後續節點,而取得操作則發生在隊尾。顯然,該結構其實是個Lock-Free佇列。 因為只有Owner執行緒才能從隊尾取元素,也即執行緒出列操作無爭用,當然也就避免了CAS的ABA問題。

EntryList

EntryList與ContentionList邏輯上同屬等待佇列,ContentionList會被執行緒併發訪問,為了降低對ContentionList隊尾的爭用,而建立EntryList。Owner執行緒在unlock時會從ContentionList中遷移執行緒到EntryList,並會指定EntryList中的某個執行緒(一般為Head)為Ready(OnDeck)執行緒,Owner執行緒並不是把鎖傳遞給OnDeck執行緒,只是把競爭鎖的權利交給OnDeck,OnDeck執行緒需要重新競爭鎖,這樣做雖然犧牲了一定的公平性,但極大地提高了整體吞吐量,在Hotspot中把OnDeck的選擇行為稱之為“競爭切換”。 OnDeck執行緒獲得鎖後即變為owner執行緒,無法獲得鎖則會依然留住EntryList中,考慮到公平性,在EntryList中的位置不發生變化(依然在隊頭)。如果Owner執行緒被wait方法阻塞,則轉移到WaitSet佇列;如果在某個時刻被notify/notifyAll喚醒,則再次轉移到EntryList。

相關文章