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
- JVM使用ACC_SYNCHRONIZED訪問標識來區分一個方法是否爲同步方法
- 在方法調用時,會檢查方法是否被設置了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
- synchronized修飾同步代碼塊時,由monitorenter和monitorexit指令來實現同步
- 進入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依賴於底層操做系統的實現,存在用戶態和內核態之間的切換,因此增長了性能開銷
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;
}
鎖升級優化
- 爲了提高性能,在JDK 1.6引入偏向鎖、輕量級鎖、重量級鎖,用來減小鎖競爭帶來的上下文切換
- 藉助JDK 1.6新增的Java對象頭,實現了鎖升級功能
Java對象頭
- 在JDK 1.6的JVM中,對象實例在堆內存中被分爲三部分:對象頭、實例數據、對齊填充
- 對象頭的組成部分:Mark Word、指向類的指針、數組長度(可選,數組類型時纔有)
- Mark Word記錄了對象和鎖有關的信息,在64位的JVM中,Mark Word爲64 bit
- 鎖升級功能主要依賴於Mark Word中鎖標誌位和是否偏向鎖標誌位
- synchronized同步鎖的升級優化路徑:偏向鎖 -> 輕量級鎖 -> 重量級鎖
偏向鎖
- 偏向鎖主要用來優化同一線程屢次申請同一個鎖的競爭,在某些狀況下,大部分時間都是同一個線程競爭鎖資源
- 偏向鎖的做用
- 當一個線程再次訪問同一個同步代碼時,該線程只需對該對象頭的Mark Word中去判斷是否有偏向鎖指向它
- 無需再進入Monitor去競爭對象(避免用戶態和內核態的切換)
- 當對象被當作同步鎖,並有一個線程搶到鎖時
- 鎖標誌位仍是01,是否偏向鎖標誌位設置爲1,而且記錄搶到鎖的線程ID,進入偏向鎖狀態
- 偏向鎖不會主動釋放鎖
- 當線程1再次獲取鎖時,會比較當前線程的ID與鎖對象頭部的線程ID是否一致,若是一致,無需CAS來搶佔鎖
- 若是不一致,須要查看鎖對象頭部記錄的線程是否存活
- 若是沒有存活,那麼鎖對象被重置爲無鎖狀態(也是一種撤銷),而後從新偏向線程2
- 若是存活,查找線程1的棧幀信息
- 若是線程1仍是須要繼續持有該鎖對象,那麼暫停線程1(STW),撤銷偏向鎖,升級爲輕量級鎖
- 若是線程1再也不使用該鎖對象,那麼將該鎖對象設爲無鎖狀態(也是一種撤銷),而後從新偏向線程2
- 一旦出現其餘線程競爭鎖資源時,偏向鎖就會被撤銷
- 偏向鎖的撤銷可能須要等待全局安全點,暫停持有該鎖的線程,同時檢查該線程是否還在執行該方法
- 若是尚未執行完,說明此刻有多個線程競爭,升級爲輕量級鎖;若是已經執行完畢,喚醒其餘線程繼續CAS搶佔
- 在高併發場景下,當大量線程同時競爭同一個鎖資源時,偏向鎖會被撤銷,發生STW,加大了性能開銷
- 默認配置
- -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=4000
- 默認開啓偏向鎖,而且延遲生效,由於JVM剛啓動時競爭很是激烈
- 關閉偏向鎖
- -XX:-UseBiasedLocking
- 直接設置爲重量級鎖
- -XX:+UseHeavyMonitors
紅線流程部分:偏向鎖的獲取和撤銷
輕量級鎖
- 當有另一個線程競爭鎖時,因爲該鎖處於偏向鎖狀態
- 發現對象頭Mark Word中的線程ID不是本身的線程ID,該線程就會執行CAS操做獲取鎖
- 若是獲取成功,直接替換Mark Word中的線程ID爲本身的線程ID,該鎖會保持偏向鎖狀態
- 若是獲取失敗,說明當前鎖有必定的競爭,將偏向鎖升級爲輕量級鎖
- 線程獲取輕量級鎖時會有兩步
- 先把鎖對象的Mark Word複製一份到線程的棧幀中(DisplacedMarkWord),主要爲了保留現場!!
- 而後使用CAS,把對象頭中的內容替換爲線程棧幀中DisplacedMarkWord的地址
- 場景
- 在線程1複製對象頭Mark Word的同時(CAS以前),線程2也準備獲取鎖,也複製了對象頭Mark Word
- 在線程2進行CAS時,發現線程1已經把對象頭換了,線程2的CAS失敗,線程2會嘗試使用自旋鎖來等待線程1釋放鎖
- 輕量級鎖的適用場景:線程交替執行同步塊,絕大部分的鎖在整個同步週期內都不存在長時間的競爭
紅線流程部分:升級輕量級鎖
自旋鎖 / 重量級鎖
- 輕量級鎖CAS搶佔失敗,線程將會被掛起進入阻塞狀態
- 若是正在持有鎖的線程在很短的時間內釋放鎖資源,那麼進入阻塞狀態的線程被喚醒後又要從新搶佔鎖資源
- JVM提供了自旋鎖,能夠經過自旋的方式不斷嘗試獲取鎖,從而避免線程被掛起阻塞
- 從JDK 1.7開始,自旋鎖默認啓用,自旋次數不建議設置過大(意味着長時間佔用CPU)
- -XX:+UseSpinning -XX:PreBlockSpin=10
- 自旋鎖重試以後若是依然搶鎖失敗,同步鎖會升級至重量級鎖,鎖標誌位爲10
- 在這個狀態下,未搶到鎖的線程都會進入Monitor,以後會被阻塞在WaitSet中
- 在鎖競爭不激烈且鎖佔用時間很是短的場景下,自旋鎖能夠提升系統性能
- 一旦鎖競爭激烈或者鎖佔用的時間過長,自旋鎖將會致使大量的線程一直處於CAS重試狀態,佔用CPU資源
- 在高併發的場景下,能夠經過關閉自旋鎖來優化系統性能
- -XX:-UseSpinning
- 關閉自旋鎖優化
- -XX:PreBlockSpin
- 默認的自旋次數,在JDK 1.7後,由JVM控制
小結
1.JVM在JDK 1.6中引入了分級鎖機制來優化synchronized
2.當一個線程獲取鎖時,首先對象鎖成爲一個偏向鎖
- 這是爲了不在同一線程重複獲取同一把鎖時,用戶態和內核態頻繁切換
3.若是有多個線程競爭鎖資源,鎖將會升級爲輕量級鎖
- 這適用於在短期內持有鎖,且分鎖交替切換的場景
- 輕量級鎖還結合了自旋鎖來避免線程用戶態與內核態的頻繁切換
4.若是鎖競爭太激烈(自旋鎖失敗),同步鎖會升級爲重量級鎖
5.優化synchronized同步鎖的關鍵:減小鎖競爭
- 應該儘可能使synchronized同步鎖處於輕量級鎖或偏向鎖,這樣才能提升synchronized同步鎖的性能
- 經常使用手段
- 減小鎖粒度:下降鎖競爭
- 減小鎖的持有時間,提升synchronized同步鎖在自旋時獲取鎖資源的成功率,避免升級爲重量級鎖
6.在鎖競爭激烈時,能夠考慮禁用偏向鎖和禁用自旋鎖
我是小架,咱們
下篇文章見!