1?synchronized的JVM底層原理實現的精簡理解
Java 虛擬機中的synchronized基于進入和退出Monitor對象(也稱為管程或監視器鎖)實現, 無論是顯式同步(synchronized作用在同步代碼塊,有明確的 monitorenter 和 monitorexit 指令) 還是隱式同步(synchronized作用在方法區,調用指令ACC_SYNCHRONIZED 標志)都是如此,都是使得Monitor對象里面的count計數期增加或者減少來實現,然后synchronized屬于重量級鎖,效率低下,因為監視器鎖(monitor)是依賴于底層的操作系統的Mutex Lock來實現的,而操作系統實現線程之間的切換時需要從用戶態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什么早期的synchronized效率低的原因,ReentrantLock底層實現依賴于特殊的CPU指令,比如發送lock指令和unlock指令,不需要用戶態和內核態的切換,所以效率高(這里和volatile底層原理類似)。
?
?
?
?
?
?
?
?
?
?
2 Java對象頭與Monitor的理解
1)Java對象的構成
在JVM中,對象在內存中的布局分為三塊區域:對象頭、實例數據和對齊填充
- 實例變量:類的屬性數據信息,包括父類的屬性信息。
- 填充數據:虛擬機要求對象起始地址必須是8字節的整數倍,這里和C語言的結構體內存對齊還是有點不一樣。
- 對象頭:jvm中采用2個字節來存儲對象頭(如果對象是數組則會分配3個字,多出來的1個字記錄的是數組長度),其主要結構是由Mark Word 和 Class Metadata Address?
虛擬機位數? ? ? ? ? 頭對象結構? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 作用
32/64bit? ? ? ? ? ? ? ?Mark Word? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?存儲對象的hashCode、鎖信息或分代年齡或GC標志等信息
32/64bit? ? ? ? ? ? ? ?Class Metadata Address? ? ? ? ?類型指針指向對象的類元數據,JVM通過這個指針確定該對象是哪個類的實例。
?
32位JVM的Mark Word默認存儲結構如下
鎖狀態 | 25bit | 4bit | 1bit是否是偏向鎖 | 2bit 鎖標志位 |
---|---|---|---|---|
無鎖狀態 | 對象HashCode | 對象分代年齡 | 0 | 01 |
Mark Word默認存儲結構外,還有如下可能變化的結構
?
?
?
?
?
?
2)monitor對象
輕量級鎖和偏向鎖是Java 6 對 synchronized 鎖進行優化后新增加,重量級鎖也就是通常說synchronized的對象鎖,鎖標識位為10,其中指針指向的是monitor對象,每個對象都存在著一個 monitor 與之關聯,對象與其 monitor 之間的關系有存在多種實現方式,如monitor可以與對象一起創建銷毀或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有后,它便處于鎖定狀態。在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的,其主要數據結構如下(位于HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的)
?
ObjectMonitor() {_header = NULL;_count = 0; //記錄個數_waiters = 0,_recursions = 0;_object = NULL;_owner = NULL;_WaitSet = NULL; //處于wait狀態的線程,會被加入到_WaitSet_WaitSetLock = 0 ;_Responsible = NULL ;_succ = NULL ;_cxq = NULL ;FreeNext = NULL ;_EntryList = NULL ; //處于等待鎖block狀態的線程,會被加入到該列表_SpinFreq = 0 ;_SpinClock = 0 ;OwnerIsThread = 0 ;}
ObjectMonitor中有兩個隊列,_WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合,當線程獲取到對象的monitor 后進入 _Owner 區域并把monitor中的owner變量設置為當前線程同時monitor中的計數器count加1,若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復為null,count自減1,同時該線程進入 WaitSe t集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)并復位變量的值,以便其他線程進入獲取monitor(鎖)
monitor對象存在于每個Java對象的對象頭中(存儲的指針的指向),synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因,同時也是notify/notifyAll/wait等方法存在于頂級對象Object中的原因
?
?
?
?
?
?
?
?
?
?
3?synchronized作用于代碼塊
?synchronized作用代碼塊后反編譯的字節碼關鍵如下
3: monitorenter //進入同步方法
//..........省略其他
15: monitorexit //退出同步方法
16: goto 24
//省略其他.......
21: monitorexit //退出同步方法
從字節碼中可知同步語句塊的實現使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結束位置,當執行monitorenter指令時,當前線程將試圖獲取 objectref(即對象鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數器為 0,那線程可以成功取得 monitor,并將計數器值設置為 1,取鎖成功。如果當前線程已經擁有 objectref 的 monitor 的持有權,那它可以重入這個 monitor (關于重入性稍后會分析),重入時計數器的值也會加 1。倘若其他線程已經擁有 objectref 的 monitor 的所有權,那當前線程將被阻塞,直到正在執行線程執行完畢,即monitorexit指令被執行,執行線程將釋放 monitor(鎖)并設置計數器值為0 ,其他線程將有機會持有 monitor 。值得注意的是編譯器將會確保無論方法通過何種方式完成,方法中調用過的每條 monitorenter 指令都有執行其對應 monitorexit 指令,而無論這個方法是正常結束還是異常結束。為了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執行 monitorexit 指令。從字節碼中也可以看出多了一個monitorexit指令,它就是異常結束時被執行的釋放monitor 的指令
?
?
?
?
?
?
?
?
?
?
?
4?synchronized作用于方法
synchronized作用代碼塊后反編譯的字節碼關鍵如下
descriptor: ()V//方法標識ACC_PUBLIC代表public修飾,ACC_SYNCHRONIZED指明該方法為同步方法flags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:
?JVM可以從方法常量池中的方法表結構(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標志區分一個方法是否同步方法。當方法調用時,調用指令將會 檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設置,如果設置了,執行線程將先持有monitor(虛擬機規范中用的是管程一詞), 然后再執行方法,最后再方法完成(無論是正常完成還是非正常完成)時釋放monitor。在方法執行期間,執行線程持有了monitor,其他任何線程都無法再獲得同一個monitor。如果一個同步方法執行期間拋 出了異常,并且在方法內部無法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法之外時自動釋放
?
?
?
?
?
?
?
?
?
?
?
?
5?synchronized的優化
鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖
其膨脹方向:無鎖——>偏向鎖——>輕量級鎖——>重量級鎖,并且膨脹方向不可逆。?
1)? 偏向鎖:
核心思想是,如果一個線程獲得了鎖,那么鎖就進入偏向模式,此時Mark Word 的結構也變為偏向鎖結構,當這個線程再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從而也就提供程序的性能,對于沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個線程申請相同的鎖。但是對于鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的線程都是不相同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗后,并不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。下面我們接著了解輕量級鎖
?
2)??輕量級鎖:
“對絕大部分的鎖,在整個同步周期內都不存在競爭”,注意這是經驗數據。需要了解的是,輕量級鎖所適應的場景是線程交替執行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹為重量級鎖。
?
?
?
3) 、重量級鎖
重量級鎖是由輕量級鎖升級而來,當同一時間有多個線程競爭鎖時,鎖就會被升級成重量級鎖,此時其申請鎖帶來的開銷也就變大
?
?
?
4)?、鎖消除
消除鎖是虛擬機另外一種鎖的優化,這種優化更徹底,在JIT編譯時,對運行上下文進行掃描,去除不可能存在競爭的鎖
部分參考博客:https://blog.csdn.net/javazejian/article/details/72828483
?