您好,登錄后才能下訂單哦!
這篇文章主要講解了“怎么理解synchronized與鎖的關系”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“怎么理解synchronized與鎖的關系”吧!
JVM 是如何實現 synchronized 的?
我知道可以利用 synchronized 關鍵字來給程序進行加鎖,但是它具體怎么實現的我不清楚呀,別急,咱們先來看個 demo :
public class demo { public void synchronizedDemo(Object lock){ synchronized(lock){ lock.hashCode(); } } }
上面是我寫的一個 demo ,然后進入到 class 文件所在的目錄下,使用 javap -v demo.class 來看一下編譯的字節碼(在這里我截取了一部分):
public void synchronizedDemo(java.lang.Object); descriptor: (Ljava/lang/Object;)V flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=2 0: aload_1 1: dup 2: astore_2 3: monitorenter 4: aload_1 5: invokevirtual #2 // Method java/lang/Object.hashCode:()I 8: pop 9: aload_2 10: monitorexit 11: goto 19 14: astore_3 15: aload_2 16: monitorexit 17: aload_3 18: athrow 19: return Exception table: from to target type 4 11 14 any 14 17 14 any
應該能夠看到當程序聲明 synchronized 代碼塊時,編譯成的字節碼會包含 monitorenter和 monitorexit 指令,這兩種指令會消耗操作數棧上的一個引用類型的元素(也就是 synchronized 關鍵字括號里面的引用),作為所要加鎖解鎖的鎖對象。如果看的比較仔細的話,上面有一個 monitorenter 指令和兩個 monitorexit 指令,這是 Java 虛擬機為了確保獲得的鎖不管是在正常執行路徑,還是在異常執行路徑上都能夠解鎖。
關于 monitorenter 和 monitorexit ,可以理解為每個鎖對象擁有一個鎖計數器和一個指向持有該鎖的線程指針:
當程序執行 monitorenter 時,如果目標鎖對象的計數器為 0 ,說明這個時候它沒有被其他線程所占有,此時如果有線程來請求使用, Java 虛擬機就會分配給該線程,并且把計數器的值加 1
目標鎖對象計數器不為 0 時,如果鎖對象持有的線程是當前線程, Java 虛擬機可以將其計數器加 1 ,如果不是呢?那很抱歉,就只能等待,等待持有線程釋放掉
當執行 monitorexit 時, Java 虛擬機就將鎖對象的計數器減 1 ,當計數器減到 0 時,說明這個鎖就被釋放掉了,此時如果有其他線程來請求,就可以請求成功
為什么采用這種方式呢?是為了允許同一個線程重復獲取同一把鎖。比如,一個 Java 類中擁有好多個 synchronized 方法,那這些方法之間的相互調用,不管是直接的還是間接的,都會涉及到對同一把鎖的重復加鎖操作。這樣去設計的話,就可以避免這種情況。
鎖
在 Java 多線程中,所有的鎖都是基于對象的。也就是說, Java 中的每一個對象都可以作為一個鎖。你可能會有疑惑,不對呀,不是還有類鎖嘛。但是 class 對象也是特殊的 Java 對象,所以呢,在 Java 中所有的鎖都是基于對象的
在 Java6 之前,所有的鎖都是"重量級"鎖,重量級鎖會帶來一個問題,就是如果程序頻繁獲得鎖釋放鎖,就會導致性能的極大消耗。為了優化這個問題,引入了"偏向鎖"和"輕量級鎖"的概念。所以在 Java6 及其以后的版本,一個對象有 4 種鎖狀態:無鎖狀態,偏向鎖狀態,輕量級鎖狀態,重量級鎖狀態。
在 4 種鎖狀態中,無鎖狀態應該比較好理解,無鎖就是沒有鎖,任何線程都可以嘗試修改,所以這里就一筆帶過了。
隨著競爭情況的出現,鎖的升級非常容易發生,但是如果想要讓鎖降級,條件非常苛刻,有種你想來可以,但是想走不行的趕腳。
阿粉在這里啰嗦一句:很多文章說,鎖如果升級之后是不能降級的,其實在 HotSpot JVM 中,是支持鎖降級的
鎖降級發生在 Stop The World 期間,當 JVM 進入安全點的時候,會檢查有沒有閑置的鎖,如果有就會嘗試進行降級
看到 Stop The World 和 安全點 可能有人比較懵,我這里簡單說一下,具體還需要讀者自己去探索一番.(因為這是 JVM 的內容,這篇文章的重點不是 JVM )
在 Java 虛擬機里面,傳統的垃圾回收算法采用的是一種簡單粗暴的方式,就是 Stop-the-world ,而這個 Stop-the-world 就是通過安全點( safepoint )機制來實現的,安全點是什么意思呢?就是 Java 程序在執行本地代碼時,如果這段代碼不訪問 Java 對象/調用 Java 方法/返回到原來的 Java 方法,那 Java 虛擬機的堆棧就不會發生改變,這就代表執行的這段本地代碼可以作為一個安全點。當 Java 虛擬機收到 Stop-the-world 請求時,它會等所有的線程都到達安全點之后,才允許請求 Stop-the-world 的線程進行獨占工作
接下來就介紹一下幾種鎖和鎖升級
Java 對象頭
在剛開始就說了, Java 的鎖都是基于對象的,那是怎么告訴程序我是個鎖呢?就不得不來說, Java 對象頭 每個 Java 對象都有對象頭,如果是非數組類型,就用 2 個字寬來存儲對象頭,如果是數組,就用 3 個字寬來存儲對象頭。在 32 位處理器中,一個字寬是 32 位;在 64 位處理器中,字寬就是 64 位咯~對象頭的內容就是下面這樣:
長度 | 內容 | 說明 |
---|---|---|
32/64 bit | Mark Word | 存儲對象的 hashCode 或鎖信息等 |
32/64 bit | Class Metadata Address | 存儲到對象類型數據的指針 |
32/64 bit | Array length | 數組的長度(如果是數組) |
咱們主要來看 Mark Word 的內容:
鎖狀態 | 29 bit/61 bit | 1 bit 是否是偏向鎖 | 2 bit 鎖標志位 |
---|---|---|---|
無鎖 | 0 | 01 | |
偏向鎖 | 線程 ID | 1 | 01 |
輕量級鎖 | 指向棧中鎖記錄的指針 | 此時這一位不用于標識偏向鎖 | 00 |
重量級鎖 | 指向互斥量(重量級鎖)的指針 | 此時這一位不用于標識偏向鎖 | 10 |
GC 標記 | 此時這一位不用于標識偏向鎖 | 11 |
從上面表格中,應該能夠看到,是偏向鎖時, Mark Word 存儲的是偏向鎖的線程 ID ;是輕量級鎖時, Mark Word 存儲的是指向線程棧中 Lock Record 的指針;是重量級鎖時, Mark Word 存儲的是指向堆中的 monitor 對象的指針
偏向鎖
HotSpot 的作者經過大量的研究發現,在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得
基于此,就引入了偏向鎖的概念
所以啥是偏向鎖呢?用大白話說就是,我現在給鎖設置一個變量,當一個線程請求的時候,發現這個鎖是 true ,也就是說這個時候沒有所謂的資源競爭,那也不用走什么加鎖/解鎖的流程了,直接拿來用就行。但是如果這個鎖是 false 的話,說明存在其他線程競爭資源,那咱們再走正規的流程
看一下具體的實現原理:
當一個線程第一次進入同步塊時,會在對象頭和棧幀中的鎖記錄中存儲鎖偏向的線程 ID 。當下次該線程進入這個同步塊時,會檢查鎖的 Mark Word 里面存放的是不是自己的線程 ID。如果是,說明線程已經獲得了鎖,那么這個線程在進入和退出同步塊時,都不需要花費 CAS 操作來加鎖和解鎖;如果不是,說明有另外一個線程來競爭這個偏向鎖,這時就會嘗試使用 CAS 來替換 Mark Word 里面的線程 ID 為新線程的 ID 。此時會有兩種情況:
替換成功,說明之前的線程不存在了,那么 Mark Word 里面的線程 ID 為新線程的 ID ,鎖不會升級,此時仍然為偏向鎖
替換失敗,說明之前的線程仍然存在,那就暫停之前的線程,設置偏向鎖標識為 0 ,并設置鎖標志位為 00 ,升級為輕量級鎖,按照輕量級鎖的方式進行競爭鎖
撤銷偏向鎖
偏向鎖使用了一種等到競爭出現時才釋放鎖的機制。也就說,如果沒有人來和我競爭鎖的時候,那么這個鎖就是我獨有的,當其他線程嘗試和我競爭偏向鎖時,我會釋放這個鎖
在偏向鎖向輕量級鎖升級時,首先會暫停擁有偏向鎖的線程,重置偏向鎖標識,看起來這個過程挺簡單的,但是開銷是很大的,因為:
首先需要在一個安全點停止擁有鎖的線程
然后遍歷線程棧,如果存在鎖記錄的話,就需要修復鎖記錄和 Mark Word ,變成無鎖狀態
最后喚醒被停止的線程,把偏向鎖升級成輕量級鎖
你以為就是升級一個輕量級鎖?too young too simple
偏向鎖向輕量級鎖升級的過程中,是非常耗費資源的,如果應用程序中所有的鎖通常都處于競爭狀態,偏向鎖此時就是一個累贅,此時就可以通過 JVM 參數關閉偏向鎖: -XX:-UseBiasedLocking=false ,那么程序默認會進入輕量級鎖狀態
最后,來張圖吧~
輕量級鎖
如果多個線程在不同時段獲取同一把鎖,也就是不存在鎖競爭的情況,那么 JVM 就會使用輕量級鎖來避免線程的阻塞與喚醒
輕量級鎖加鎖
JVM 會為每個線程在當前線程的棧幀中創建用于存儲鎖記錄的空間,稱之為 Displaced Mark Word 。如果一個線程獲得鎖的時候發現是輕量級鎖,就會將鎖的 Mark Word 復制到自己的 Displaced Mark Word 中。之后線程會嘗試用 CAS 將鎖的 Mark Word 替換為指向鎖記錄的指針。
如果替換成功,當前線程獲得鎖,那么整個狀態還是 輕量級鎖 狀態
如果替換失敗了呢?說明 Mark Word 被替換成了其他線程的鎖記錄,那就嘗試使用自旋來獲取鎖.(自旋是說,線程不斷地去嘗試獲取鎖,一般都是用循環來實現的)
自旋是耗費 CPU 的,如果一直獲取不到鎖,線程就會一直自旋, CPU 那么寶貴的資源就這么被白白浪費了
解決這個問題最簡單的辦法就是指定自旋的次數,比如如果沒有替換成功,那就循環 10 次,還沒有獲取到,那就進入阻塞狀態
但是 JDK 采用了一個更加巧妙的方法---適應性自旋。就是說,如果這次線程自旋成功了,那我下次自旋次數更多一些,因為我這次自旋成功,說明我成功的概率還是挺大的,下次自旋次數就更多一些,那么如果自旋失敗了,下次我自旋次數就減少一些,就比如,已經看到了失敗的前兆,那我就先溜,而不是非要“不撞南墻不回頭”
自旋失敗之后,線程就會阻塞,同時鎖會升級成重量級鎖
輕量級鎖釋放:
在釋放鎖時,當前線程會使用 CAS 操作將 Displaced Mark Word 中的內容復制到鎖的 Mark Word 里面。如果沒有發生競爭,這個復制的操作就會成功;如果有其他線程因為自旋多次導致輕量級鎖升級成了重量級鎖, CAS 操作就會失敗,此時會釋放鎖同時喚醒被阻塞的過程
同樣,來一張圖吧:
重量級鎖
重量級鎖依賴于操作系統的互斥量( mutex )來實現。但是操作系統中線程間狀態的轉換需要相對比較長的時間(因為操作系統需要從用戶態切換到內核態,這個切換成本很高),所以重量級鎖效率很低,但是有一點就是,被阻塞的線程是不會消耗 CPU 的
每一個對象都可以當做一個鎖,那么當多個線程同時請求某個對象鎖時,它會怎么處理呢?
對象鎖會設置集中狀態來區分請求的線程:
Contention List:所有請求鎖的線程將被首先放置到該競爭隊列
Entry List: Contention List 中那些有資格成為候選人的線程被移到 Entry List 中
Wait Set:調用 wait 方法被阻塞的線程會被放置到 Wait Set 中
OnDeck:任何時刻最多只能有一個線程正在競爭鎖,該線程稱為 OnDeck
Owner:獲得鎖的線程稱為 Owner
!Owner:釋放鎖的線程
當一個線程嘗試獲得鎖時,如果這個鎖被占用,就會把該線程封裝成一個 ObjectWaiter對象插入到 Contention List 隊列的隊首,然后調用 park 函數掛起當前線程
當線程釋放鎖時,會從 Contention List 或者 Entry List 中挑選一個線程進行喚醒
如果線程在獲得鎖之后,調用了 Object.wait 方法,就會將該線程放入到 WaitSet 中,當被 Object.notify 喚醒后,會將線程從 WaitSet 移動到 Contention List 或者 Entry List 中。
但是,當調用一個鎖對象的 wait 或 notify 方法時,如果當前鎖的狀態是偏向鎖或輕量級鎖,則會先膨脹成重量級鎖
感謝各位的閱讀,以上就是“怎么理解synchronized與鎖的關系”的內容了,經過本文的學習后,相信大家對怎么理解synchronized與鎖的關系這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。