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."<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.在鎖競爭激烈時,可以考慮禁用偏向鎖和禁用自旋鎖

我是小架,我們

下篇文章見!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM