Java性能之synchronized鎖的優化

synchronized / Lock

1.JDK 1.5以前,Java經過synchronized關鍵字來實現鎖功能java

  • 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."<init>":()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修飾同步代碼塊時,由monitorenter和monitorexit指令來實現同步
  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.在鎖競爭激烈時,能夠考慮禁用偏向鎖和禁用自旋鎖

我是小架,咱們

下篇文章見!

相關文章
相關標籤/搜索