目錄
- synchronized底層原理(總結版)
- `synchronized` 底層原理(詳解版)
- 1. 字節碼層面:monitorenter 和 monitorexit
- 2. JVM 底層實現:對象頭與 Monitor
- 2.1 Java 對象頭(Mark Word)
- 2.2 Monitor(管程/監視器鎖)
- 3. 鎖的升級與優化
- 3.1 偏向鎖
- 3.2 輕量級鎖
- 3.3 重量級鎖
- 4. 硬件層面:內存屏障與 CAS
- JVM鎖升級是什麼?
- 對象的內存結構
- MarkWord
- 再説Monitor重量級鎖
- 輕量級鎖
- 偏向鎖
synchronized底層原理(總結版)
synchronize底層使用的是minitor,Monitor 被翻譯為監視器,是由jvm提供,c++語言實現。
使用javap -v xxx.class反編譯一段代碼可以看到機器指令
- monitorenter 上鎖開始的地方
- monitorexit 解鎖的地方
- 其中被monitorenter和monitorexit包圍住的指令就是上鎖的代碼
- 第二個monitorexit是為了防止鎖住的代碼拋異常後不能及時釋放鎖
monitor主要就是跟這個對象產生關聯,如下圖
Monitor內部具體的存儲結構:
- Owner:存儲當前獲取鎖的線程,只能有一個線程可以獲取
- EntryList:關聯沒有搶到鎖的線程,處於Blocked狀態的線程
- WaitSet:關聯調用了wait方法的線程,處於Waiting狀態的線程
具體的流程:
- 進入synchorized代碼塊時,先讓lock(對象鎖)關聯monitor,然後判斷Owner是否有線程持有
- 如果沒有線程持有,則讓當前線程持有,表示該線程獲取鎖成功
- 如果有線程持有,則讓當前線程進入entryList進行阻塞,如果Owner持有的線程已經釋放了鎖,在EntryList中的線程去競爭鎖的持有權(非公平)
- 如果代碼塊中調用了wait()方法,則會進去WaitSet中進行等待
參考回答:
- Synchronized【對象鎖】採用互斥的方式讓同一時刻至多隻有一個線程能持有【對象鎖】
- 它的底層由monitor實現的,monitor是jvm級別的對象( C++實現),線程獲得鎖需要使用對象(鎖)關聯monitor
- 在monitor內部有三個屬性,分別是owner、entrylist、waitset
- 其中owner是關聯的獲得鎖的線程,並且只能關聯一個線程;entrylist關聯的是處於阻塞狀態的線程;waitset關聯的是處於Waiting狀態的線程好的,我們來詳細、深入地剖析一下
synchronized在 JVM 中的底層實現原理。這對於理解 Java 併發編程至關重要。
synchronized 底層原理(詳解版)
synchronized 的底層原理可以從三個層面來看:字節碼層面、JVM 底層實現 和 硬件層面。我們逐層深入。
1. 字節碼層面:monitorenter 和 monitorexit
當我們使用 synchronized 關鍵字時,無論是修飾代碼塊還是方法,在編譯後的字節碼中都會生成對應的指令。
- 同步代碼塊:
對於synchronized(object) { ... },編譯器會在同步代碼塊的前後分別生成monitorenter和monitorexit指令。
public void method() {
synchronized (obj) {
// 同步代碼塊
System.out.println("hello");
}
}
編譯後的字節碼大致如下:
public void method();
Code:
0: aload_0
1: getfield #2 // 獲取對象引用 obj
4: dup
5: astore_1
6: monitorenter // 進入同步塊,嘗試獲取鎖
7: getstatic #3 // 獲取 System.out
10: ldc #4 // 加載 "hello"
12: invokevirtual #5 // 調用 println
15: aload_1
16: monitorexit // 正常退出同步塊,釋放鎖
17: goto 25
20: astore_2
21: aload_1
22: monitorexit // 異常退出同步塊,釋放鎖 (確保在異常情況下也能釋放鎖)
23: aload_2
24: athrow
25: return
關鍵點:
- 可以看到有兩個 `monitorexit` 指令,第一個用於正常退出,第二個用於處理異常情況(隱藏在 `finally` 語義中),這確保了即使同步塊內拋出異常,鎖也能被正確釋放。
- 同步方法:
對於synchronized修飾的方法,方法常量池中會設置ACC_SYNCHRONIZED標誌。
public synchronized void method() {
// 方法體
}
- 當方法調用時,調用指令(如 `invokevirtual`)會檢查這個標誌。如果設置了,執行線程會先嚐試獲取鎖(對於實例方法是 `this`,對於靜態方法是該類的 `Class` 對象),再執行方法體。方法執行完畢後,無論是正常返回還是異常拋出,都會自動釋放鎖。
小結:從字節碼看,synchronized 的實現依賴於 monitorenter 和 monitorexit 這一對指令,或者方法的 ACC_SYNCHRONIZED 標誌。
2. JVM 底層實現:對象頭與 Monitor
monitorenter 和 monitorexit 指令背後的具體實現,是 JVM 的核心。其關鍵在於 Java 對象頭 和 Monitor。
2.1 Java 對象頭(Mark Word)
在 HotSpot 虛擬機中,每個 Java 對象在內存中存儲的佈局分為三部分:對象頭、實例數據、對齊填充。
其中,對象頭 是理解鎖的關鍵。它包含兩部分信息:
- Mark Word:存儲對象自身的運行時數據,如哈希碼、GC 分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID 等。它是實現鎖的“主戰場”。
- Klass Pointer:對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
為了在極小的空間內存儲儘可能多的信息,Mark Word 被設計成一個非固定的動態數據結構。它會根據對象的狀態複用自己的存儲空間。下圖清晰地展示了 32 位 JVM 下 Mark Word 在不同狀態下的結構:
(在 64 位 JVM 下,結構類似,只是空間更大。)
關鍵點:注意最後 2 位(lock),它標識了對象的鎖狀態。鎖的升級過程就體現在這 2 位的變化上。
2.2 Monitor(管程/監視器鎖)
JVM 為每個對象都關聯了一個內置的 Monitor(管程)。monitorenter 指令的本質就是嘗試去獲取這個對象對應的 Monitor。
一個 Monitor 由以下部分組成:
- Owner:當前持有該 Monitor 的線程。初始為
null。 - EntryList:處於
Blocked狀態的、等待鎖的線程隊列。當 Owner 釋放鎖時,JVM 會從 EntryList 中挑選一個線程來成為新的 Owner。 - WaitSet:處於
Waiting狀態的、調用了Object.wait()方法的線程隊列。這些線程在等待其他線程的通知(notify/notifyAll)。
工作流程:
- 當線程執行到
monitorenter指令時,會嘗試進入(enter)該對象的 Monitor。 - 如果 Monitor 的 Owner 為
null,則該線程成功成為 Owner,並將鎖的計數器 +1。 - 如果該線程已經是 Owner(可重入鎖),它再次進入,鎖計數器再次 +1。
- 如果 Owner 是其他線程,則當前線程會進入 EntryList,進入
BLOCKED狀態,直到 Owner 線程釋放鎖。 - 當線程執行
monitorexit指令時,鎖計數器 -1。當計數器減到 0 時,線程釋放 Monitor,不再擔任 Owner。然後,EntryList 中的線程會開始競爭鎖。
3. 鎖的升級與優化
在 Java 6 之前,synchronized 是一個重量級鎖,性能較差,因為它依賴於操作系統的 Mutex Lock(互斥鎖),需要進行用户態到內核態的切換,耗時較長。
為了減少這種性能開銷,Java 6 引入了鎖升級機制。synchronized 的鎖狀態從低到高分為四種,升級路徑是單向的:無鎖 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖。
3.1 偏向鎖
- 目的:在沒有競爭的情況下,消除整個同步操作。假設在大多數情況下,鎖不僅不存在競爭,而且總是由同一線程多次獲得。
- 原理:
- 當一個線程訪問同步塊時,會在對象頭和棧幀中的鎖記錄裏存儲偏向的線程 ID。
- 以後該線程再次進入和退出同步塊時,不需要進行 CAS 操作來加鎖和解鎖,只需簡單測試一下對象頭的 Mark Word 裏是否存儲着指向當前線程的偏向鎖。
- 如果測試成功,表示線程已經獲得了鎖。
- 撤銷:一旦出現另一個線程來嘗試競爭鎖,偏向模式就宣告結束。持有偏向鎖的線程會被掛起,JVM 會撤銷偏向鎖,然後升級為輕量級鎖。
注意:在 Java 15 之後,偏向鎖被標記為廢棄並默認禁用,因為維護其帶來的收益已不如從前。但理解其原理依然重要。
3.2 輕量級鎖
- 目的:在競爭不激烈(“近交替執行”)的情況下,避免直接使用重量級鎖帶來的性能消耗。
- 加鎖過程:
- 在當前線程的棧幀中創建一個名為 鎖記錄 的空間。
- 將對象頭的 Mark Word 複製到鎖記錄中(稱為 Displaced Mark Word)。
- 線程嘗試使用 CAS 操作將對象頭的 Mark Word 替換為指向鎖記錄的指針。
- 如果成功,當前線程獲得鎖。並將對象 Mark Word 的最後 2 位設置為
00,表示輕量級鎖狀態。 - 如果失敗,表示存在競爭(另一個線程也修改了 Mark Word)。
- 解鎖過程:
- 使用 CAS 操作將 Displaced Mark Word 替換回對象頭。
- 如果成功,則同步過程順利完成。
- 如果失敗,説明鎖已經升級,需要釋放鎖的同時喚醒被掛起的線程。
3.3 重量級鎖
- 觸發條件:當輕量級鎖競爭失敗後,會自旋嘗試獲取鎖一定次數(自旋鎖)。如果自旋後依然失敗,鎖就會膨脹為重量級鎖。
- 特點:
- 此時 Mark Word 中存儲的是指向重量級鎖(Monitor)的指針。
- 等待鎖的線程都會進入 EntryList,進入
BLOCKED狀態。 - 依賴於操作系統底層的 Mutex Lock,需要進行用户態到內核態的切換,成本最高。
4. 硬件層面:內存屏障與 CAS
synchronized 的語義保證了原子性、可見性和有序性。
- 可見性與有序性:是通過在編譯器和處理器層面插入 內存屏障 來實現的。在同步塊開始時加
Load Barrier,在同步塊結束時加Store Barrier,強制將工作內存中的修改刷新到主內存,並禁止指令重排序。 - 原子性:對於簡單的
monitorenter/monitorexit,由 Monitor 保證。對於鎖升級過程中的狀態變更(如輕量級鎖的獲取),則是通過 CAS 操作實現的。CAS 是一條 CPU 原子指令(cmpxchg),它保證了“比較-交換”操作的原子性。
JVM鎖升級是什麼?
Monitor實現的鎖屬於重量級鎖,你瞭解過鎖升級嗎?
- Monitor實現的鎖屬於重量級鎖,裏面涉及到了用户態和內核態的切換、進程的上下文切換,成本較高,性能比較低。
- 在JDK 1.6引入了兩種新型鎖機制:偏向鎖和輕量級鎖,它們的引入是為了解決在沒有多線程競爭或基本沒有競爭的場景下因使用傳統鎖機制帶來的性能開銷問題。
對象的內存結構
MarkWord
我們可以通過lock的標識,來判斷是哪一種鎖的等級
- 後三位是001表示無鎖
- 後三位是101表示偏向鎖
- 後兩位是00表示輕量級鎖
- 後兩位是10表示重量級鎖
再説Monitor重量級鎖
每個對象的markword都可以設置monoitor的指針,讓對象與monitor產生關聯
輕量級鎖
**加鎖的流程 **
1.在線程棧中創建一個Lock Record,將其obj字段指向鎖對象。
2.通過CAS指令將Lock Record的地址存儲在對象頭的mark word中(數據進行交換),如果對象處於無鎖狀態代表該線程獲得了輕量級鎖。
3.如果是當前線程已經持有該鎖了,代表這是一次鎖重入。設置Lock Record第一部分為null,起到了一個重入計數器的作用。
4.如果CAS修改失敗,説明發生了競爭,需要膨脹為重量級鎖。
解鎖過程
1.遍歷線程棧,找到所有obj字段等於當前鎖對象的Lock Record。
2.如果Lock Record的Mark Word為null,代表這是一次重入,將obj設置為null後continue。
3.如果Lock Record的 Mark Word不為null,則利用CAS指令將對象頭的mark word 恢復成為無鎖狀態。如果失敗則膨脹為重量級鎖。
偏向鎖
輕量級鎖在沒有競爭時(就自己這個線程),每次重入仍然需要執行 CAS 操作。
Java 6 中引入了偏向鎖來做進一步優化:只有第一次使用 CAS 將線程 ID 設置到對象的 Mark Word 頭,之後發現這個線程 ID 是自己的就表示沒有競爭,不用重新 CAS。以後只要不發生競爭,這個對象就歸該線程所有.
**加鎖的流程 **
1.在線程棧中創建一個Lock Record,將其obj字段指向鎖對象。
2.通過CAS指令將Lock Record的線程id存儲在對象頭的mark word中,同時也設置偏向鎖的標識為101,如果對象處於無鎖狀態則修改成功,代表該線程獲得了偏向鎖
3.如果是當前線程已經持有該鎖了,代表這是一次鎖重入。設置Lock Record第一部分為null,起到了一個重入計數器的作用。與輕量級鎖不同的時,這裏不會再次進行cas操作,只是判斷對象頭中的線程id是否是自己,因為缺少了cas操作,性能相對輕量級鎖更好一些