Synchronized关键字和锁升级
一、Synchronized
對于多線程不安全(當數據共享(臨界資源),而多線程同時訪問并改變該數據時,就會不安全),JAVA提供的鎖有兩個,一個是synchronized關鍵字,另外一個就是lock類。JDK1.6之前,synchronized是一個重量級鎖,在使用非自旋鎖(互斥鎖)時,“阻塞或喚醒一個Java線程需要操作系統切換CPU狀態來完成,這種狀態轉換需要耗費處理器時間。如果同步代碼塊中的內容過于簡單,狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長”。這種方式就是synchronized最初實現同步的方式,這就是JDK 6之前synchronized效率低的原因。JDK1.6之后,synchronized進行了很大的優化,加入了偏置鎖、輕量級鎖、自旋鎖等,大大提高了synchronized的性能。
JAVA鎖綜述:
- 悲觀鎖:關系型數據庫的讀鎖,讀都要加鎖
- 樂觀鎖:讀的時候不加鎖,但是需要有版本號,例如Redis,SVN
- 共享鎖(讀鎖)和排它鎖(寫鎖)是悲觀鎖的不同的實現,它倆都屬于悲觀鎖的范疇。
- 阻塞鎖:多個線程同時調用同一個方法的時候,所有線程都被排隊處理了。讓線程進入阻塞狀態進行等待,當獲得相應的信號(喚醒,時間) 時,才可以進入線程的準備就緒狀態,準備就緒狀態的所有線程,通過競爭,進入運行狀態。
- 非阻塞鎖:多個線程同時調用一個方法的時候,當某一個線程最先獲取到鎖,這時其他線程判斷沒拿到鎖,這時就直接返回,只有當最先獲取到鎖的線程釋放,其他線程才能進來,在它釋放之前其它線程都會獲取失敗。
1.Synchronized 的作用主要有三個(原子性、可見性、有序性):
- 確保線程互斥的訪問同步代碼- 保證共享變量的修改能夠及時可見- 有效解決重排序問題2.Synchronized可以把任何一個非null對象作為"鎖",在HotSpot JVM實現中,鎖有個專門的名字:對象監視器(Object Monitor)。從語法上講,Synchronized 總共有三種用法:
- 當synchronized作用在實例方法時,監視器鎖(monitor)便是對象實例(this);- 當synchronized作用在靜態方法時,監視器鎖(monitor)便是對象的Class實例,因為Class數據存在于永久代,因此靜態方法鎖相當于該類的一個全局鎖;- 當synchronized作用在某一個對象實例時,監視器鎖(monitor)便是括號括起來的對象實例;3.線程五大狀態
- 新建狀態start:新建線程對象,并沒有調用start()方法之前
- 就緒狀態waiting:調用start()方法之后線程就進入就緒狀態,但是并不是說只要調用start()方法線程就馬上變為當前線程,在變為當前線程之前都是為就緒狀態。值得一提的是,線程在睡眠和掛起中恢復的時候也會進入就緒狀態哦。
- 運行狀態running:線程被設置為當前線程,開始執行run()方法。就是線程進入運行狀態
- 阻塞狀態blocking:線程被暫停,比如說調用sleep()方法后線程就進入阻塞狀態
- 死亡狀態dead:線程執行結束
二、Synchronized底層實現
1.先了解(Java對象頭)概念:
我們都知道對象是存放在堆內存中的,大致可以分為三個部分,分別是對象頭、實例變量和填充字節。
一圖了解:
Epoch:偏向鎖時間戳
2.對象監視器(monitor)
重量級鎖對應的鎖標志位是10,存儲了指向重量級監視器鎖的指針,在Hotspot中,對象的監視器(monitor)鎖對象由ObjectMonitor對象實現(C++),其跟同步相關的數據結構如下:
線程(synchronized修飾的方法(代碼塊))在獲取鎖的幾個狀態的轉換:
- 1.當多個線程同時訪問該方法,那么這些線程會先被放進_EntryList隊列,此時線程處于blocking狀態
- 2.當一個線程獲取到了實例對象的監視器(monitor)鎖,那么就可以進入running狀態,執行方法,此時,ObjectMonitor對象的_owner指向當前線程,_count加1表示當前對象鎖被一個線程獲取
- 3.當running狀態的線程調用wait()方法,那么當前線程釋放monitor對象,進入waiting狀態,ObjectMonitor對象的_owner變為null,_count減1,同時線程進入_WaitSet隊列,直到有線程調用notify()方法喚醒該線程,則該線程重新獲取monitor對象進入_Owner區
- 4.如果當前線程執行完畢,那么也釋放monitor對象,進入waiting狀態,ObjectMonitor對象的_owner變為null,_count減1
3.monitor結構及配合重量鎖的流程圖
4.Synchronized修飾的代碼塊/方法如何獲取monitor對象
(1)Synchronized修飾代碼塊:
Synchronized代碼塊同步在需要同步的代碼塊開始的位置插入monitorentry指令,在同步結束的位置或者異常出現的位置插入monitorexit指令;JVM要保證monitorentry和monitorexit都是成對出現的,任何對象都有一個monitor與之對應,當這個對象的monitor被持有以后,它將處于鎖定狀態。
例如,同步代碼塊如下:
public class SyncCodeBlock {public int i;public void syncTask(){synchronized (this){i++;}} }對同步代碼塊編譯后的class字節碼文件反編譯,結果如下(僅保留方法部分的反編譯內容)
public void syncTask();descriptor: ()Vflags: ACC_PUBLICCode:stack=3, locals=3, args_size=10: aload_01: dup2: astore_13: monitorenter //注意此處,進入同步方法4: aload_05: dup6: getfield #2 // Field i:I9: iconst_110: iadd11: putfield #2 // Field i:I14: aload_115: monitorexit //注意此處,退出同步方法16: goto 2419: astore_220: aload_121: monitorexit //注意此處,退出同步方法22: aload_223: athrow24: returnException table://省略其他字節碼.......(2)Synchronized修飾方法:
Synchronized方法同步不再是通過插入monitorentry和monitorexit指令實現,而是由方法調用指令來讀取運行時常量池中的ACC_SYNCHRONIZED標志隱式實現的,如果方法表結構(method_info Structure)中的ACC_SYNCHRONIZED標志被設置,那么線程在執行方法前會先去獲取對象的monitor對象,如果獲取成功則執行方法代碼,執行完畢后釋放monitor對象,如果monitor對象已經被其它線程獲取,那么當前線程被阻塞。
同步方法代碼如下:
public class SyncMethod {public int i;public synchronized void syncTask(){i++;} }對同步方法編譯后的class字節碼反編譯,結果如下(僅保留方法部分的反編譯內容):
public synchronized void syncTask();descriptor: ()V//方法標識ACC_PUBLIC代表public修飾,ACC_SYNCHRONIZED指明該方法為同步方法flags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=3, locals=1, args_size=10: aload_01: dup2: getfield #2 // Field i:I5: iconst_16: iadd7: putfield #2 // Field i:I10: returnLineNumberTable:line 12: 0line 13: 10 }可以看出方法開始和結束的地方都沒有出現monitorentry和monitorexit指令,但是出現的ACC_SYNCHRONIZED標志位。
三、鎖的優化(升級)
鎖的狀態:無鎖狀態、偏向鎖狀態(鎖只被一個線程持有)、輕量級鎖狀態(不同線程交替持有鎖)、重量級鎖狀態(多線程競爭鎖)(級別從低到高)
1.無鎖狀態:不會鎖住資源,多個線程只有一個能成功,其他重試
2.使用偏向鎖:同一個線程執行同步資源時自動獲取資源
最關鍵的判斷A線程是否存在的c++源碼分析。
- 偏向鎖的撤銷
偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著,如果線程不處于活動狀態,則將對象頭設置成無鎖狀態,如果線程仍然活著,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要么重新偏向于其他線程,要么恢復到無鎖或者標記對象不適合作為偏向鎖,最后喚醒暫停的線程。
偏向鎖在Java 6和Java 7里是默認啟用的,但是它在應用程序啟動幾秒鐘之后才激活,如有必要可以使用JVM參數來關閉延遲-XX:BiasedLockingStartupDelay = 0。如果你確定自己應用程序里所有的鎖通常情況下處于競爭狀態,可以通過JVM參數關閉偏向鎖-XX:-UseBiasedLocking=false,那么默認會進入輕量級鎖狀態。
- 釋放和撤銷的區別
釋放:對應的就是synchronized方法的退出或synchronized塊的結束。
撤銷:籠統的說就是多個線程競爭導致不能再使用偏向模式的時候。
這里強調:偏向鎖的撤銷(Revoke) 操作并不是將對象恢復到無鎖可偏向的狀態, 而是在偏向鎖的獲取過程中, 發現了競爭時, 直接將一個被偏向的對象“升級到” 被加了輕量級鎖的狀態
3.偏向鎖->輕量級鎖:多個線程競爭同步資源時,沒有獲取到資源的線程自旋等待鎖釋放
按照輕量級鎖的定義,如果對象的鎖級別升級為輕量級鎖后,JVM將在線程A、線程B和之后請求獲得對象Y操作的若干線程的當前棧幀中,添加一個鎖記錄空間(Lock record),并將對象頭中的Mark Word復制到鎖記錄中。然后線程會持續嘗試使用CAS原理將對象頭中的Mark Word部分替換為指向本線程鎖記錄空間的指針。如果替換成功則當前線程獲得這個對象的操作權;如果多次CAS持續失敗,說明當前對象的多線程搶占現象很嚴重,這是對象鎖升級為重量鎖狀態,并使用操作系統層面的Mutex Lock(互斥鎖)技術進行實現。
示意圖:
4.輕量級鎖->重量級鎖
重量級鎖依賴于操作系統的互斥量(mutex) 實現, 該操作會導致進程從用戶態與內核態之間的切換, 是一個開銷較大的操作
- 獲取重量鎖流程:
- 釋放重量鎖流程:
5.三鎖總結:
偏向鎖中:MarkWord->threadID 輕量級鎖:MarkWord->線程棧中的LockRecord地址 重量級鎖:MarkWord->一個堆中的monitor對象的指針四、鎖的粗化
鎖粗化
如果在一段代碼中連續的對同一個對象反復加鎖解鎖,其實是相對耗費資源的,這種情況下可以適當放寬加鎖的范圍,減少性能消耗。
當 JIT 發現一系列連續的操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作出現在循環體中的時候,會將加鎖同步的范圍擴散到整個操作序列的外部。
五、鎖的消除
為了保證數據的完整性,在進行操作時需要對這部分操作進行同步控制,但是在有些情況下,JVM檢測到不可能存在共享數據競爭,這是JVM會對這些同步鎖進行鎖消除。
鎖消除的依據是逃逸分析的數據支持如果不存在競爭,為什么還需要加鎖呢?所以鎖消除可以節省毫無意義的請求鎖的時間。變量是否逃逸,對于虛擬機來說需要使用數據流分析來確定,但是對于程序員來說這還不清楚么?在明明知道不存在數據競爭的代碼塊前加上同步嗎?但是有時候程序并不是我們所想的那樣?雖然沒有顯示使用鎖,但是在使用一些JDK的內置API時,如StringBuffer、Vector、HashTable等,這個時候會存在隱形的加鎖操作。比如StringBuffer的append()方法,Vector的add()方法:
//在運行這段代碼時,JVM可以明顯檢測到變量vector沒有逃逸出方法vectorTest()之外, //所以JVM可以大膽地將vector內部的加鎖操作消除。 public void vectorTest(){Vector<String> vector = new Vector<String>();for(int i = 0 ; i < 10 ; i++){vector.add(i + "");}System.out.println(vector); }參考文獻:
https://blog.csdn.net/lengxiao1993/article/details/81568130
https://juejin.im/post/5d96db806fb9a04e0f30f0eb
https://www.cnblogs.com/paddix/p/5405678.html
https://blog.csdn.net/tongdanping/article/details/79647337#2%E3%80%81%E9%94%81%E7%B2%97%E5%8C%96
https://www.cnblogs.com/webor2006/p/11442551.html
https://juejin.im/post/5b4eec7df265da0fa00a118f#heading-10
總結
以上是生活随笔為你收集整理的Synchronized关键字和锁升级的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 互联网日报 | 7月8日 星期四 | 小
- 下一篇: 2021垂直类电商私域化洞察报告