深入了解Java?Synchronized鎖升級過程
前言
首先,synchronized 是什么?我們需要明確的給個定義——同步鎖,沒錯,它就是把鎖。
可以用來干嘛?鎖,當然當然是用于線程間的同步,以及保護臨界區(qū)內的資源。我們知道,鎖是個非?;\統(tǒng)的概念,像生活中有指紋鎖、密碼鎖等等多個種類,那 synchronized 代表的鎖具體是把什么鎖呢?
答案是—— Java 內置鎖。在 Java 中,每個對象中都隱藏著一把鎖,而 synchronized 關鍵字就是激活這把隱式鎖的把手(開關)。
先來簡單了解一下 synchronized,我們知道其共有 3 種使用方式:

- 修飾靜態(tài)方法:鎖住當前 class,作用于該 class 的所有實例
- 修飾非靜態(tài)方法:只會鎖住當前 class 的實例
- 修飾代碼塊:該方法接受一個對象作為參數,鎖住的即該對象
使用方法就不在這里贅述,可自行搜索其詳細的用法,這不是本篇文章所關心的內容。
知道了 synchronized 的概念,回頭來看標題,它說的鎖升級到底是個啥?對于不太熟悉鎖升級的人來說,可能會想:
所謂鎖,不就是啪一下鎖上就完事了嗎?升級是個什么玩意?這跟打撲克牌也沒關系啊。
對于熟悉的人來說,可能會想:
不就是「無鎖 ==> 偏向鎖 ==> 輕量級鎖 ==> 重量級鎖 」嗎?
你可能在很多地方看到過上面描述的鎖升級過程,也能直接背下來。但你真的知道無鎖、偏向鎖、輕量級鎖、重量級鎖到底代表著什么嗎?這些鎖存儲在哪里?以及什么情況下會使得鎖向下一個 level 升級?
想知道答案,我們似乎必須先搞清楚 Java 內置鎖,其內部結構是啥樣的?內置鎖又存放在哪里?
答案在開篇提到過——在 Java 對象中。
那么現在的問題就從「內置鎖結構是啥」變成了「Java 對象長啥樣」。
對象結構
從宏觀上看,Java 對象的結構很簡單,分為三部分:

從微觀上看,各個部分都還可以深入展開,詳見下圖:

接下來分別深入討論一下這三部分。
對象頭
從腦圖中可以看出,其由 Mark Word、Class Pointer、數組長度三個字段組成。簡單來說:
- Mark Word:主要用于存儲自身運行時數據
- Class Pointer:是指針,指向方法區(qū)中該 class 的對象,JVM 通過此字段來判斷當前對象是哪個類的實例
- 數組長度:當且僅當對象是數組時才會有該字段
Class Pointer 和數組長度沒什么好說的,接下來重點聊聊 Mark Word。
Mark Word 所代表的「運行時數據」主要用來表示當前 Java 對象的線程鎖狀態(tài)以及 GC 的標志。而線程鎖狀態(tài)分別就是無鎖、偏向鎖、輕量級鎖、重量級鎖。
所以前文提到的這 4 個狀態(tài),其實就是 Java 內置鎖的不同狀態(tài)。
在 JDK 1.6 之前,內置鎖都是重量級鎖,效率低下。效率低下表現在
而在 JDK 1.6 之后為了提高 synchronized 的效率,才引入了偏向鎖、輕量級鎖。
隨著鎖競爭逐漸激烈,其狀態(tài)會按照「無鎖 ==> 偏向鎖 ==> 輕量級鎖 ==> 重量級鎖 」這個方向逐漸升級,并且不可逆,只能進行鎖升級,而無法進行鎖降級。
接下來我們思考一個問題,既然 Mark Word 可以表示 4 種不同的鎖狀態(tài),其內部到底是怎么區(qū)分的呢?(由于目前主流的 JVM 都是 64 位,所以我們只討論 64 位的 Mark Word)接下來我們通過圖片直觀的感受一下。
(1)無鎖

這個可以理解為單線程很快樂的運行,沒有其他的線程來和其競爭。
(2)偏向鎖

首先,什么叫偏向鎖?舉個例子,一段同步的代碼,一直只被線程 A 訪問,既然沒有其他的線程來競爭,每次都要獲取鎖豈不是浪費資源?所以這種情況下線程 A 就會自動進入偏向鎖的狀態(tài)。
后續(xù)線程 A 再次訪問同步代碼時,不需要做任何的 check,直接執(zhí)行(對該線程的「偏愛」),這樣降低了獲取鎖的代價,提升了效率。
看到這里,你會發(fā)現無鎖、偏向鎖的 lock 標志位是一樣的,即都是 01,這是因為無鎖、偏向鎖是靠字段 biased_lock 來區(qū)分的,0 代表沒有使用偏向鎖,1 代表啟用了偏向鎖。為什么要這么搞?你可以理解為無鎖、偏向鎖在本質上都可以理解為無鎖(參考上面提到的線程 A 的狀態(tài)),所以 lock 的標志位都是 01 是沒毛病的。
PS:這里的線程 ID 是持有當前對象偏向鎖的線程
(3)輕量級鎖

但是,一旦有第二個線程參與競爭,就會立即膨脹為輕量級鎖。企圖搶占的線程一開始會使用自旋:
的方式去嘗試獲取鎖。如果循環(huán)幾次,其他的線程釋放了鎖,就不需要進行用戶態(tài)到內核態(tài)的切換。雖然如此,但自旋需要占用很多 CPU 的資源(自行理解汽車空檔瘋狂踩油門)。如果另一個線程 一直不釋放鎖,難道它就在這一直空轉下去嗎?
當然不可能,JDK 1.7 之前是普通自旋,會設定一個最大的自旋次數,默認是 10 次,超過這個閾值就停止自旋。JDK 1.7 之后,引入了適應性自旋。簡單來說就是:這次自旋獲取到鎖了,自旋的次數就會增加;這次自旋沒拿到鎖,自旋的次數就會減少。
(4)重量級鎖

上面提到,試圖搶占的線程自旋達到閾值,就會停止自旋,那么此時鎖就會膨脹成重量級鎖。當其膨脹成重量級鎖后,其他競爭的線程進來就不會自旋了,而是直接阻塞等待,并且 Mark Word 中的內容會變成一個監(jiān)視器(monitor)對象,用來統(tǒng)一管理排隊的線程。
這個 monitor 對象,每個對象都會關聯(lián)一個。monitor 對象本質上是一個同步機制,保證了同時只有一個線程能夠進入臨界區(qū),在 HotSpot 的虛擬機中,是由 C++ 類 ObjectMonitor 實現的。
那么 monitor 對象具體是如何來管理線程的?接下來我們看幾個 ObjectMonitor 類關鍵的屬性:
- ContentionQueue:是個隊列,所有競爭鎖的線程都會先進入這個隊列中,可以理解為線程的統(tǒng)一入口,進入的線程會阻塞。
- EntryList:ContentionQueue 中有資格的線程會被移動到這里,相當于進行一輪初篩,進入的線程會阻塞。
- Owner:擁有當前 monitor 對象的線程,即 —— 持有鎖的那個線程。
- OnDeck:與 Owner 線程進行競爭的線程,同一時刻只會有一個 OnDeck 線程在競爭。
- WaitSet:當 Owner 線程調用 wait() 方法被阻塞之后,會被放到這里。當其被喚醒之后,會重新進入 EntryList 當中,這個集合的線程都會阻塞。
- Count:用于實現可重入鎖,synchronized 是可重入的。
對象體
對象體包含了當前對象的字段和值,在業(yè)務中u l是較為核心的部分。
對齊字節(jié)
就是單純用于填充的字節(jié),沒有其他的業(yè)務含義。其目的是為了保證對象所占用的內存大小為 8 的倍數,因為HotSpot VM 的內存管理要求對象的起始地址必須是 8 的倍數。
鎖升級
了解完 4 種鎖狀態(tài)之后,我們就可以整體的來看一下鎖升級的過程了。
線程 A 進入 synchronized 開始搶鎖,JVM 會判斷當前是否是偏向鎖的狀態(tài),如果是就會根據 Mark Word 中存儲的線程 ID 來判斷,當前線程 A 是否就是持有偏向鎖的線程。如果是,則忽略 check,線程 A 直接執(zhí)行臨界區(qū)內的代碼。
但如果 Mark Word 里的線程不是線程 A,就會通過自旋嘗試獲取鎖,如果獲取到了,就將 Mark Word 中的線程 ID 改為自己的;如果競爭失敗,就會立馬撤銷偏向鎖,膨脹為輕量級鎖。
后續(xù)的競爭線程都會通過自旋來嘗試獲取鎖,如果自旋成功那么鎖的狀態(tài)仍然是輕量級鎖。然而如果競爭失敗,鎖會膨脹為重量級鎖,后續(xù)等待的競爭的線程都會被阻塞。
補充:Synchronized底層原理
Synchronized被編譯后會生成monitorenter 和 monitorexit 指令。
在執(zhí)行monitorenter時,會嘗試獲取對象的鎖,如果鎖的計數器為 0 則表示鎖可以被獲取,獲取后將鎖計數器設為 1 也就是加 1。
在執(zhí)行 monitorexit 指令后,將鎖計數器設為 0,表明鎖被釋放。如果獲取對象鎖失敗,那當前線程就要阻塞等待,直到鎖被另外一個線程釋放為止。
Monitor是依賴于操作系統(tǒng)的mutex lock實現的,每當掛起或者喚醒1個線程都要切換操作系統(tǒng)內核態(tài)。這個操作是比較重量級的,在一些情況下甚至切換時間本身會超出線程執(zhí)行任務的時間。
synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實是 ACC_SYNCHRONIZED 標識,該標識指明了該方法是一個同步方法。
EOF
其實偏向鎖還有一個撤銷的過程,也是有代價的,但相比于偏向鎖帶好的好處,是能夠接受的。但我們這里重點的還是關注鎖升級的具體邏輯和細節(jié),關于鎖升級的過程就聊到這里。
到此這篇關于Java Synchronized鎖升級過程的文章就介紹到這了,更多相關Java Synchronized鎖升級內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringBoot 使用 OpenAPI3 規(guī)范整合 knife4j的詳細過程
Swagger工具集使用OpenAPI規(guī)范,可以生成、展示和測試基于OpenAPI規(guī)范的API文檔,并提供了生成客戶端代碼的功能,本文給大家介紹SpringBoot使用OpenAPI3規(guī)范整合knife4j的詳細過程,感興趣的朋友跟隨小編一起看看吧2023-12-12
Springboot配置suffix指定mvc視圖的后綴方法
這篇文章主要介紹了Springboot配置suffix指定mvc視圖的后綴方法,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07
springboot中使用Hibernate-Validation校驗參數詳解
這篇文章主要為大家介紹了springboot中使用Hibernate-Validation校驗參數詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07
elasticsearch中term與match的區(qū)別講解
今天小編就為大家分享一篇關于elasticsearch中term與match的區(qū)別講解,小編覺得內容挺不錯的,現在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-02-02

