jvm細節(jié)探索之synchronized及實現(xiàn)問題分析
在C程序代碼中我們可以利用操作系統(tǒng)提供的互斥鎖來實現(xiàn)同步塊的互斥訪問及線程的阻塞及喚醒等工作。然而在Java中除了提供LockAPI外還在語法層面上提供了synchronized關鍵字來實現(xiàn)互斥同步原語。那么到底在JVM內部是怎么實現(xiàn)synchronized關鍵子的呢?
一、synchronized的字節(jié)碼表示:
在java語言中存在兩種內建的synchronized語法:1、synchronized語句;2、synchronized方法。對于synchronized語句當Java源代碼被javac編譯成bytecode的時候,會在同步塊的入口位置和退出位置分別插入monitorenter和monitorexit字節(jié)碼指令。而synchronized方法則會被翻譯成普通的方法調用和返回指令如:invokevirtual、areturn指令,在VM字節(jié)碼層面并沒有任何特別的指令來實現(xiàn)被synchronized修飾的方法,而是在Class文件的方法表中將該方法的access_flags字段中的synchronized標志位置1,表示該方法是同步方法并使用調用該方法的對象或該方法所屬的Class在JVM的內部對象表示Klass做為鎖對象。
二、JVM中鎖的優(yōu)化:
簡單來說在JVM中monitorenter和monitorexit字節(jié)碼依賴于底層的操作系統(tǒng)的MutexLock來實現(xiàn)的,但是由于使用MutexLock需要將當前線程掛起并從用戶態(tài)切換到內核態(tài)來執(zhí)行,這種切換的代價是非常昂貴的;然而在現(xiàn)實中的大部分情況下,同步方法是運行在單線程環(huán)境(無鎖競爭環(huán)境)如果每次都調用MutexLock那么將嚴重的影響程序的性能。不過在jdk1.6中對鎖的實現(xiàn)引入了大量的優(yōu)化,如鎖粗化(LockCoarsening)、鎖消除(LockElimination)、輕量級鎖(LightweightLocking)、偏向鎖(BiasedLocking)、適應性自旋(AdaptiveSpinning)等技術來減少鎖操作的開銷。
鎖粗化(LockCoarsening):也就是減少不必要的緊連在一起的unlock,lock操作,將多個連續(xù)的鎖擴展成一個范圍更大的鎖。
鎖消除(LockElimination):通過運行時JIT編譯器的逃逸分析來消除一些沒有在當前同步塊以外被其他線程共享的數(shù)據(jù)的鎖保護,通過逃逸分析也可以在線程本地Stack上進行對象空間的分配(同時還可以減少Heap上的垃圾收集開銷)。
輕量級鎖(LightweightLocking):這種鎖實現(xiàn)的背后基于這樣一種假設,即在真實的情況下我們程序中的大部分同步代碼一般都處于無鎖競爭狀態(tài)(即單線程執(zhí)行環(huán)境),在無鎖競爭的情況下完全可以避免調用操作系統(tǒng)層面的重量級互斥鎖,取而代之的是在monitorenter和monitorexit中只需要依靠一條CAS原子指令就可以完成鎖的獲取及釋放。當存在鎖競爭的情況下,執(zhí)行CAS指令失敗的線程將調用操作系統(tǒng)互斥鎖進入到阻塞狀態(tài),當鎖被釋放的時候被喚醒(具體處理步驟下面詳細討論)。
偏向鎖(BiasedLocking):是為了在無鎖競爭的情況下避免在鎖獲取過程中執(zhí)行不必要的CAS原子指令,因為CAS原子指令雖然相對于重量級鎖來說開銷比較小但還是存在非??捎^的本地延遲(可參考這篇文章)。
適應性自旋(AdaptiveSpinning):當線程在獲取輕量級鎖的過程中執(zhí)行CAS操作失敗時,在進入與monitor相關聯(lián)的操作系統(tǒng)重量級鎖(mutexsemaphore)前會進入忙等待(Spinning)然后再次嘗試,當嘗試一定的次數(shù)后如果仍然沒有成功則調用與該monitor關聯(lián)的semaphore(即互斥鎖)進入到阻塞狀態(tài)。
三、對象頭(ObjectHeader):

在JVM中創(chuàng)建對象時會在對象前面加上兩個字大小的對象頭,在32位機器上一個字為32bit,根據(jù)不同的狀態(tài)位MarkWorld中存放不同的內容,如上圖所示在輕量級鎖中,MarkWord被分成兩部分,剛開始時LockWord為被設置為HashCode、最低三位表示LockWord所處的狀態(tài),初始狀態(tài)為001表示無鎖狀態(tài)。Klassptr指向Class字節(jié)碼在虛擬機內部的對象表示的地址。Fields表示連續(xù)的對象實例字段。
四、MonitorRecord:
MonitorRecord是線程私有的數(shù)據(jù)結構,每一個線程都有一個可用monitorrecord列表,同時還有一個全局的可用列表;那么這些monitorrecord有什么用呢?每一個被鎖住的對象都會和一個monitorrecord關聯(lián)(對象頭中的LockWord指向monitorrecord的起始地址,由于這個地址是8byte對齊的所以LockWord的最低三位可以用來作為狀態(tài)位),同時monitorrecord中有一個Owner字段存放擁有該鎖的線程的唯一標識,表示該鎖被這個線程占用。如下圖所示為MonitorRecord的內部結構:

Owner:初始時為NULL表示當前沒有任何線程擁有該monitor record,當線程成功擁有該鎖后保存線程唯一標識,當鎖被釋放時又設置為NULL;
EntryQ:關聯(lián)一個系統(tǒng)互斥鎖(semaphore),阻塞所有試圖鎖住monitor record失敗的線程。
RcThis:表示blocked或waiting在該monitor record上的所有線程的個數(shù)。
Nest:用來實現(xiàn)重入鎖的計數(shù)。
HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。
Candidate:用來避免不必要的阻塞或等待線程喚醒,因為每一次只有一個線程能夠成功擁有鎖,如果每次前一個釋放鎖的線程喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒然后因為競爭鎖失敗又被阻塞)從而導致性能嚴重下降。Candidate只有兩種可能的值0表示沒有需要喚醒的線程1表示要喚醒一個繼任線程來競爭鎖。
五、輕量級鎖具體實現(xiàn):
一個線程能夠通過兩種方式鎖住一個對象:1、通過膨脹一個處于無鎖狀態(tài)(狀態(tài)位001)的對象獲得該對象的鎖;2、對象已經(jīng)處于膨脹狀態(tài)(狀態(tài)位00)但LockWord指向的monitor record的Owner字段為NULL,則可以直接通過CAS原子指令嘗試將Owner設置為自己的標識來獲得鎖。
獲取鎖(monitorenter)的大概過程如下:
(1)當對象處于無鎖狀態(tài)時(RecordWord值為HashCode,狀態(tài)位為001),線程首先從自己的可用moniter record列表中取得一個空閑的moniter record,初始Nest和Owner值分別被預先設置為1和該線程自己的標識,一旦monitor record準備好然后我們通過CAS原子指令安裝該monitor record的起始地址到對象頭的LockWord字段來膨脹(原文為inflate,我覺得之所以叫inflate主要是由于當對象被膨脹后擴展了對象的大?。粸榱丝臻g效率,將monitor record結構從對象頭中抽出去,當需要的時候才將該結構attach到對象上,但是和這篇Paper有點互相矛盾,兩種實現(xiàn)方式稍微有點不同)該對象,如果存在其他線程競爭鎖的情況而調用CAS失敗,則只需要簡單的回到monitorenter重新開始獲取鎖的過程即可。
(2)對象已經(jīng)被膨脹同時Owner中保存的線程標識為獲取鎖的線程自己,這就是重入(reentrant)鎖的情況,只需要簡單的將Nest加1即可。不需要任何原子操作,效率非常高。
(3)對象已膨脹但Owner的值為NULL,當一個鎖上存在阻塞或等待的線程同時鎖的前一個擁有者剛釋放鎖時會出現(xiàn)這種狀態(tài),此時多個線程通過CAS原子指令在多線程競爭狀態(tài)下試圖將Owner設置為自己的標識來獲得鎖,競爭失敗的線程在則會進入到第四種情況(4)的執(zhí)行路徑。
(4)對象處于膨脹狀態(tài)同時Owner不為NULL(被鎖住),在調用操作系統(tǒng)的重量級的互斥鎖之前先自旋一定的次數(shù),當達到一定的次數(shù)時如果仍然沒有成功獲得鎖,則開始準備進入阻塞狀態(tài),首先將rfThis的值原子性的加1,由于在加1的過程中可能會被其他線程破壞Object和monitor record之間的關聯(lián),所以在原子性加1后需要再進行一次比較以確保LockWord的值沒有被改變,當發(fā)現(xiàn)被改變后則要重新進行monitorenter過程。同時再一次觀察Owner是否為NULL,如果是則調用CAS參與競爭鎖,鎖競爭失敗則進入到阻塞狀態(tài)。
釋放鎖(monitorexit)的大概過程如下:
(1)首先檢查該對象是否處于膨脹狀態(tài)并且該線程是這個鎖的擁有者,如果發(fā)現(xiàn)不對則拋出異常;
(2)檢查Nest字段是否大于1,如果大于1則簡單的將Nest減1并繼續(xù)擁有鎖,如果等于1,則進入到第(3)步;
(3)檢查rfThis是否大于0,設置Owner為NULL然后喚醒一個正在阻塞或等待的線程再一次試圖獲取鎖,如果等于0則進入到第(4)步
(4)縮?。╠eflate)一個對象,通過將對象的LockWord置換回原來的HashCode值來解除和monitor record之間的關聯(lián)來釋放鎖,同時將monitor record放回到線程是有的可用monitor record列表。
總結
參考:《深入理解Java虛擬機 JVM高級特性與最佳實踐(周志明) 》
以上就是本文關于jvm細節(jié)探索之synchronized及實現(xiàn)問題分析的全部內容,希望對大家有所幫助。如有不足之處,歡迎留言指出。感謝朋友們對本站的支持!
相關文章
java用靜態(tài)工廠代替構造函數(shù)使用方法和優(yōu)缺點
這篇文章主要介紹了java用靜態(tài)工廠代替構造函數(shù)使用方法和優(yōu)缺點,需要的朋友可以參考下2014-02-02
mybatis 集合嵌套查詢和集合嵌套結果的區(qū)別說明
這篇文章主要介紹了mybatis 集合嵌套查詢和集合嵌套結果的區(qū)別說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09
springboot如何接收復雜參數(shù)(同時接收JSON與文件)
文章介紹了在Spring Boot中同時處理JSON和文件上傳時使用`@RequestPart`注解的方法,`@RequestPart`可以接收多種格式的參數(shù),包括JSON和文件,并且可以作為`multipart/form-data`格式中的key2025-02-02
Java設計模式之單例模式Singleton Pattern詳解
這篇文章主要介紹了Java設計模式之單例模式Singleton Pattern詳解,一些常用的工具類、線程池、緩存,數(shù)據(jù)庫,數(shù)據(jù)庫連接池、賬戶登錄系統(tǒng)、配置文件等程序中可能只允許我們創(chuàng)建一個對象,這就需要單例模式,需要的朋友可以參考下2023-12-12
Maven install 報錯"程序包不存在"問題的解決方法
這篇文章主要介紹了Maven install 報錯"程序包不存在"問題的解決方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-07-07

