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
- 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."":()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.在鎖競爭激烈時,可以考慮禁用偏向鎖和
禁用自旋鎖我是小架,我們
下篇文章見!