Java性能之synchronized鎖的優化

synchronized / Lock

1.JDK 1.5之前,Java通過synchronized關鍵字來實現功能

  • synchronized是JVM實現的內置鎖,鎖的獲取和釋放都是由JVM隱式實現的

2.JDK 1.5,併發包中新增了Lock接口來實現鎖功能

  • 提供了與synchronized類似的同步功能,但需要顯式獲取和釋放鎖

3. Lock同步鎖是基於Java實現的,而synchronized是基於底層操作系統的Mutex Lock實現的

  • 每次獲取和釋放鎖都會帶來
    用戶態和內核態的切換,從而增加系統的性能開銷
  • 在鎖競爭激烈的情況下,synchronized同步鎖的性能很糟糕
  • JDK 1.5,在單線程重複申請鎖的情況下,synchronized鎖性能要比Lock的性能差很多

4.JDK 1.6,Java對synchronized同步鎖做了充分的優化,甚至在某些場景下,它的性能已經超越了Lock同步鎖

實現原理

public class SyncTest {
 public synchronized void method1() {
 }
 public void method2() {
 Object o = new Object();
 synchronized (o) {
 }
 }
}
$ javac -encoding UTF-8 SyncTest.java
$ javap -v SyncTest

修飾方法

public synchronized void method1();
 descriptor: ()V
 flags: ACC_PUBLIC, ACC_SYNCHRONIZED
 Code:
 stack=0, locals=1, args_size=1
 0: return
  1. JVM使用ACC_SYNCHRONIZED訪問標識來區分一個方法是否為
    同步方法
  2. 在方法調用時,會檢查方法是否被設置了ACC_SYNCHRONIZED訪問標識
  • 如果是,執行線程會將先嚐試持有Monitor對象,再執行方法,方法執行完成後,最後釋放Monitor對象

修飾代碼塊

public void method2();
 descriptor: ()V
 flags: ACC_PUBLIC
 Code:
 stack=2, locals=4, args_size=1
 0: new #2 // class java/lang/Object
 3: dup
 4: invokespecial #1 // Method java/lang/Object."":()V
 7: astore_1
 8: aload_1
 9: dup
 10: astore_2
 11: monitorenter
 12: aload_2
 13: monitorexit
 14: goto 22
 17: astore_3
 18: aload_2
 19: monitorexit
 20: aload_3
 21: athrow
 22: return
  1. synchronized修飾同步代碼塊時,由monitorentermonitorexit指令來實現同步
  2. 進入monitorenter指令後,線程將持有Monitor對象,進入monitorexit指令,線程將釋放
    Monitor對象

管程模型

1.JVM中的同步是基於進入和退出管程Monitor)對象實現的

2.每個Java對象實例都會有一個Monitor,Monitor可以和Java對象實例一起被創建和銷燬

3.Monitor是由ObjectMonitor實現的,對應ObjectMonitor.hpp

4.當多個線程同時訪問一段同步代碼時,會先被放在EntryList

5.當線程獲取到Java對象的Monitor時(Monitor是依靠底層操作系統

Mutex Lock來實現互斥的)

  • 線程申請Mutex成功,則持有該Mutex,其它線程將無法獲取到該Mutex

6.進入WaitSet

  • 競爭鎖失敗的線程會進入WaitSet
  • 競爭鎖成功的線程如果調用wait方法,就會釋放當前持有的Mutex,並且該線程會進入WaitSet
  • 進入WaitSet的進程會等待下一次喚醒,然後進入EntryList重新排隊

7.

如果當前線程順利執行完方法,也會釋放Mutex

8.Monitor依賴於底層操作系統的實現,存在用戶態內核態之間切換,所以增加了性能開銷

Java性能之synchronized鎖的優化

ObjectMonitor() {
 _header = NULL;
 _count = 0; // 記錄個數
 _waiters = 0,
 _recursions = 0;
 _object = NULL;
 _owner = NULL; // 持有該Monitor的線程
 _WaitSet = NULL; // 處於wait狀態的線程,會被加入 _WaitSet
 _WaitSetLock = 0 ;
 _Responsible = NULL ;
 _succ = NULL ;
 _cxq = NULL ;
 FreeNext = NULL ;
 _EntryList = NULL ; // 多個線程訪問同步塊或同步方法,會首先被加入 _EntryList
 _SpinFreq = 0 ;
 _SpinClock = 0 ;
 OwnerIsThread = 0 ;
 _previous_owner_tid = 0;
}

鎖升級優化

  1. 為了提升性能,在JDK 1.6引入偏向鎖、輕量級鎖、重量級鎖,用來減少鎖競爭帶來的上下文切換
  2. 藉助JDK 1.6新增的Java對象頭,實現了鎖升級功能

Java對象頭

  1. JDK 1.6的JVM中,對象實例在堆內存中被分為三部分:對象頭實例數據
    對齊填充
  2. 對象頭的組成部分:Mark Word指向類的指針數組長度(可選,數組類型時才有)
  3. Mark Word記錄了對象有關的信息,在64位的JVM中,Mark Word為64 bit
  4. 鎖升級功能主要依賴於Mark Word中鎖標誌位是否偏向鎖標誌位
  5. synchronized同步鎖的升級優化路徑:偏向鎖 -> 輕量級鎖 -> 重量級鎖
Java性能之synchronized鎖的優化

偏向鎖

  1. 偏向鎖主要用來優化同一線程多次申請同一個鎖的競爭,在某些情況下,大部分時間都是同一個線程競爭鎖資源
  2. 偏向鎖的作用
  • 當一個線程再次訪問同一個同步代碼時,該線程只需對該對象頭的Mark Word中去判斷是否有偏向鎖指向它
  • 無需再進入Monitor去競爭對象(避免用戶態和內核態的切換
  1. 當對象被當做同步鎖,並有一個線程搶到鎖時
  • 鎖標誌位還是01,是否偏向鎖標誌位設置為
    1,並且記錄搶到鎖的線程ID,進入偏向鎖狀態
  1. 偏向鎖不會主動釋放鎖
  • 當線程1再次獲取鎖時,會比較當前線程的ID鎖對象頭部的線程ID是否一致,如果一致,無需CAS來搶佔鎖
  • 如果不一致,需要查看鎖對象頭部記錄的線程是否存活
  • 如果沒有存活,那麼鎖對象被重置為無鎖狀態(也是一種撤銷),然後重新偏向線程2
  • 如果存活,查找線程1的棧幀信息
  • 如果線程1還是需要繼續持有該鎖對象,那麼暫停線程1(
    STW),撤銷偏向鎖升級為輕量級鎖
  • 如果線程1不再使用該鎖對象,那麼將該鎖對象設為無鎖狀態(也是一種撤銷),然後重新偏向線程2
  1. 一旦出現其他線程競爭鎖資源時,偏向鎖就會被撤銷
  • 偏向鎖的撤銷可能需要等待全局安全點,暫停持有該鎖的線程,同時檢查該線程是否還在執行該方法
  • 如果還沒有執行完,說明此刻有多個線程競爭,升級為輕量級鎖;如果已經執行完畢,喚醒其他線程繼續CAS
    搶佔
  1. 高併發場景下,當大量線程同時競爭同一個鎖資源時,偏向鎖會被撤銷,發生STW,加大了性能開銷
  • 默認配置
  • -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=4000
  • 默認開啟偏向鎖,並且延遲生效,因為JVM剛啟動時競爭非常激烈
  • 關閉偏向鎖
  • -XX:-UseBiasedLocking
  • 直接設置為重量級鎖
  • -XX:+UseHeavyMonitors

紅線流程部分:偏向鎖的

獲取撤銷

Java性能之synchronized鎖的優化

輕量級鎖

  1. 當有另外一個線程競爭鎖時,由於該鎖處於偏向鎖狀態
  2. 發現對象頭Mark Word中的線程ID不是自己的線程ID,該線程就會執行CAS
    操作獲取鎖
  • 如果獲取成功,直接替換Mark Word中的線程ID為自己的線程ID,該鎖會保持偏向鎖狀態
  • 如果獲取失敗,說明當前鎖有一定的競爭,將偏向鎖升級為輕量級鎖
  1. 線程獲取輕量級鎖時會有兩步
  • 先把鎖對象的Mark Word複製一份到線程的棧幀中(DisplacedMarkWord),主要為了保留現場!!
  • 然後使用CAS,把對象頭中的內容替換為線程棧幀中DisplacedMarkWord的地址
  1. 場景
  • 在線程1複製對象頭Mark Word的同時(CAS之前),線程2也準備獲取鎖,也複製了對象頭Mark Word
  • 在線程2進行CAS時,發現線程1已經把對象頭換了,線程2的CAS失敗,線程2會嘗試使用自旋鎖來等待線程1釋放鎖
  1. 輕量級鎖的適用場景:線程交替執行同步塊,絕大部分的鎖在整個同步週期內都不存在長時間的競爭

紅線流程部分:升級輕量級鎖

Java性能之synchronized鎖的優化

自旋鎖 / 重量級鎖

  1. 輕量級鎖CAS搶佔失敗,線程將會被掛起進入阻塞狀態
  • 如果正在持有鎖的線程在很短的時間內釋放鎖資源,那麼進入阻塞狀態的線程被喚醒後又要重新搶佔鎖資源
  1. JVM提供了自旋鎖,可以通過自旋的方式不斷嘗試獲取鎖,從而避免線程被掛起阻塞
  2. JDK 1.7
    開始,自旋鎖默認啟用,自旋次數不建議設置過大(意味著長時間佔用CPU
  • -XX:+UseSpinning -XX:PreBlockSpin=10
  1. 自旋鎖重試之後如果依然搶鎖失敗,同步鎖會升級至重量級鎖,鎖標誌位為10
  • 在這個狀態下,未搶到鎖的線程都會進入Monitor,之後會被阻塞在WaitSet
  1. 鎖競爭不激烈鎖佔用時間非常短的場景下,自旋鎖可以提高系統性能
  • 一旦鎖競爭激烈或者鎖佔用的時間過長,自旋鎖將會導致大量的線程一直處於CAS重試狀態佔用CPU資源
  1. 高併發的場景下,可以通過關閉自旋鎖來優化系統性能
  • -XX:-UseSpinning
  • 關閉自旋鎖優化
  • -XX:PreBlockSpin
  • 默認的自旋次數,在JDK 1.7後,由JVM控制


Java性能之synchronized鎖的優化

小結

1.JVM在JDK 1.6中引入了分級鎖機制來優化synchronized

2.當一個線程獲取鎖時,首先對象鎖成為一個偏向鎖

  • 這是為了避免在同一線程重複獲取同一把鎖時,用戶態和內核態頻繁切換

3.如果有多個線程競爭鎖資源,鎖將會升級為輕量級鎖

  • 這適用於在短時間內持有鎖,且分鎖交替切換的場景
  • 輕量級鎖還結合了自旋鎖
    避免線程用戶態與內核態的頻繁切換

4.如果鎖競爭太激烈(自旋鎖失敗),同步鎖會升級為重量級鎖

5.優化synchronized同步鎖的關鍵:減少鎖競爭

  • 應該儘量使synchronized同步鎖處於輕量級鎖偏向鎖,這樣才能提高synchronized同步鎖的性能
  • 常用手段
  • 減少鎖粒度:降低鎖競爭
  • 減少鎖的持有時間,提高synchronized同步鎖在自旋時獲取鎖資源的成功率,避免升級為重量級鎖

6.在鎖競爭激烈時,可以考慮禁用偏向鎖

禁用自旋鎖

我是小架,我們

下篇文章見!


分享到:


相關文章: