Mark Word
什么是Mark Word
?
Mark Word
是Java對象頭中的一個字段,它是一個32位或64位的字段(取決于系統架構),用于存儲對象的元數據信息。這些信息包括對象的哈希碼、鎖狀態、年齡等。
Mark Word
有什么用?
Mark Word
的主要用途包括:
-
存儲哈希碼:在對象需要計算哈希碼時,可以直接從
Mark Word
中讀取,避免重復計算。 -
管理鎖狀態:
Mark Word
用于存儲對象的鎖狀態,支持多種鎖機制,如偏向鎖、輕量級鎖和重量級鎖。 -
垃圾回收:
Mark Word
中的年齡信息用于垃圾回收,幫助垃圾回收器決定何時回收對象。
Mark Word
儲存在哪?
下圖為圖示
Mark Word
是Java對象頭的一部分,存儲在對象的內存布局中。對象的內存布局通常包括以下部分:
-
Mark Word:存儲對象的元數據信息。
-
Klass Word:存儲對象的類信息。
-
對象數據:存儲對象的實際數據。
在32位系統中,Mark Word
是一個32位的字段;在64位系統中,Mark Word
是一個64位的字段。為了節省內存,Java在64位系統中使用了一種稱為“指針壓縮”的技術,將Mark Word
壓縮為32位。
結構
1. Normal(正常狀態)
-
位布局:
-
hashcode:25
:25位用于存儲對象的哈希碼。 -
age:4
:4位用于存儲對象的年齡(用于垃圾回收)。 -
biased_lock:0
:1位用于表示是否啟用偏向鎖(0表示未啟用)。 -
01
:2位用于表示鎖的狀態(01表示正常狀態)。
-
2. Biased(偏向鎖狀態)
-
位布局:
-
thread:23
:23位用于存儲偏向鎖的線程ID。 -
epoch:2
:2位用于存儲偏向鎖的紀元信息。 -
age:4
:4位用于存儲對象的年齡。 -
biased_lock:1
:1位用于表示是否啟用偏向鎖(1表示已啟用)。 -
01
:2位用于表示鎖的狀態(01表示偏向鎖狀態)。
-
3. Lightweight Locked(輕量級鎖狀態)
-
位布局:
-
ptr_to_lock_record:30
:30位用于存儲指向鎖記錄的指針。 -
00
:2位用于表示鎖的狀態(00表示輕量級鎖狀態)。
-
4. Heavyweight Locked(重量級鎖狀態)
-
位布局:
-
ptr_to_heavyweight_monitor:30
:30位用于存儲指向重量級監視器的指針。 -
10
:2位用于表示鎖的狀態(10表示重量級鎖狀態)。
-
5. Marked for GC(標記為垃圾回收狀態)
-
位布局:
-
11
:2位用于表示鎖的狀態(11表示標記為垃圾回收狀態)。
-
Monitor
Monitor 是什么?
在 Java 中,Monitor(也稱為 對象鎖 或 內置鎖)是 JVM 實現線程同步的核心機制。它用于控制對共享資源的訪問,確保在多線程環境下,對共享資源的訪問是線程安全的。
一個對象會關聯一個monitor
Monitor 的作用
Monitor 的主要作用是:
-
實現線程同步:通過控制對共享資源的訪問,確保同一時間只有一個線程可以訪問共享資源。
-
實現鎖機制:Monitor 是 Java 中
synchronized
關鍵字的底層實現。 -
管理線程等待與喚醒:Monitor 內部維護了多個隊列,用于管理等待鎖的線程和等待被通知的線程。
-
JVM 統一管理 Monitor:雖然 Monitor 是對象的一部分,但 JVM 會通過全局的機制來管理所有 Monitor 的生命周期。例如,JVM 會維護一個全局的 Monitor 列表,用于分配和回收 Monitor 對象。
-
Monitor 的分配是線程安全的:JVM 會為每個線程維護一個可用的 Monitor 列表(
free
和used
),當線程需要獲取鎖時,會從這些列表中申請 Monitor 對象。
Monitor 的結構
Monitor 是一個線程私有的數據結構,通常由 JVM 實現(如 HotSpot 虛擬機中的 ObjectMonitor
類)。它包含以下關鍵部分:
字段 | 說明 |
---|---|
_owner | 指向當前持有鎖的線程的唯一標識(如 Thread 對象) |
_EntryList | 等待獲取鎖的線程(阻塞隊列) |
_WaitSet | 等待被通知的線程(等待隊列) |
_recursions | 記錄鎖的重入次數 |
_count | 鎖的計數器(用于判斷是否為可重入鎖) |
Monitor 的生命周期
-
創建:當一個對象被
synchronized
加鎖時,JVM 會為該對象創建一個 Monitor 對象。 -
銷毀:Monitor 是與對象的生命周期一致的,當對象被垃圾回收時,Monitor 也會被銷毀。
-
統一管理:JVM 會通過全局機制來管理所有 Monitor 的生命周期,包括分配和回收。
總結
問題 | 回答 |
---|---|
Monitor 是什么? | Monitor 是 Java 中實現線程同步的核心機制,是 synchronized 關鍵字的底層實現。 |
Monitor 的作用是什么? | Monitor 的作用是實現線程同步,確保對共享資源的訪問是線程安全的。 |
Monitor 的結構是什么? | Monitor 包含 _owner 、_EntryList 、_WaitSet 、_recursions 等字段。 |
Monitor 的存儲位置在哪里? | Monitor 的地址存儲在對象頭的 Mark Word 中。 |
Monitor 是如何被創建和管理的? | Monitor 是 JVM 自動創建的,與對象的生命周期一致,由 JVM 統一管理。 |
Monitor 的作用機制是什么? | Monitor 通過 monitorenter 和 monitorexit 實現鎖的獲取和釋放,支持可重入性。 |
Monitor 的優化機制有哪些? | 包括偏向鎖、輕量級鎖和重量級鎖等優化機制。 |
synchronized
是什么
synchronized 實際是用對象鎖保證了臨界區內代碼的原子性,臨界區內的代碼對外是不可分割的,不會被線程切換所打斷。
就是說 假設現在有兩個線程,那么當線程1獲取鎖在執行的時候,當線程1的時間片用完,但是線程1里面的代碼還沒有執行完, 那么線程2就不能拿到鎖,就會進入等待狀態,當線程1執行完臨界區代碼,釋放鎖,線程2就會獲取鎖,繼續執行臨界區代碼。
?
輕量級鎖
Mark work布局
-
ptr_to_lock_record:30
:30位用于存儲指向鎖記錄的指針。 -
00
:2位用于表示鎖的狀態(00表示輕量級鎖狀態)。
輕量級鎖是Java中一種用于優化同步操作的鎖機制,其主要目的是在沒有多線程競爭的情況下,減少鎖的開銷。輕量級鎖的使用場景是當多個線程對同一個對象加鎖的時間是錯開的(即沒有競爭)時,可以使用輕量級鎖來優化。這種情況下,線程之間不會發生阻塞,從而提高了性能。
輕量級鎖的加鎖過程
當一個線程進入同步塊時,如果該對象沒有被鎖定(即鎖標志位為“01”狀態),虛擬機會在當前線程的棧幀中創建一個名為鎖記錄(Lock Record)的空間,用于存儲對象頭中的Mark Word的拷貝。然后,虛擬機會嘗試使用CAS操作將對象的Mark Word更新為指向鎖記錄的指針。
如果更新成功,表示當前線程獲得了鎖,并且對象的Mark Word的鎖標志位會被設置為“00”,表示該對象處于輕量級鎖狀態。
輕量級鎖的解鎖過程
當線程退出同步塊時,虛擬機會嘗試使用CAS操作將當前線程的鎖記錄替換回對象頭。如果替換成功,表示沒有競爭發生,同步過程完成;如果替換失敗,說明有其他線程嘗試獲取鎖,此時需要喚醒被阻塞的線程。
加鎖失敗
在輕量級鎖的加鎖過程中,加鎖失敗通常有兩種情況,而鎖重入是其中一種特殊情況。以下是詳細分析:
1. 鎖已經被其他線程持有(競爭發生)
這是最常見的加鎖失敗原因。當一個線程在嘗試獲取輕量級鎖時,發現對象的Mark Word已經被其他線程修改為“00”狀態(表示輕量級鎖狀態),說明該對象已經被其他線程加鎖,或者當前線程已經嘗試過加鎖但失敗。
-
原因:多個線程對同一個對象進行同步操作,且加鎖時間重疊,導致競爭。
-
處理方式:輕量級鎖會嘗試升級為重量級鎖,通過操作系統級的互斥量來實現真正的互斥。
2. 鎖重入(Reentrant Lock)
鎖重入是輕量級鎖的一種特殊情況,指的是同一個線程對同一個對象多次加鎖。在輕量級鎖中,每次重入時,鎖記錄中會存儲一個重入計數器,表示該線程對鎖的持有次數。
-
判斷方式:在加鎖時,如果發現對象的Mark Word指向的是當前線程的棧幀,說明是鎖重入,直接繼續執行同步代碼即可。
-
處理方式:在鎖重入時,鎖記錄中的
displaced_header
會被置為NULL
,表示這是一個重入的鎖記錄。解鎖時,如果發現displaced_header
為NULL
,則說明是鎖重入,不需要恢復Mark Word,只需遞減重入計數器即可。
重量級鎖
Mark Work布局
-
ptr_to_heavyweight_monitor:30
:30位用于存儲指向重量級監視器的指針。 -
10
:2位用于表示鎖的狀態(10表示重量級鎖狀態)。
在多線程環境下,Java中的鎖機制會根據競爭情況動態調整鎖的狀態,從無鎖到偏向鎖、輕量級鎖,最終到重量級鎖。當多個線程競爭同一個對象鎖時,輕量級鎖可能會膨脹為重量級鎖。以下是詳細的鎖膨脹過程:
初始狀態
-
輕量級鎖:當一個線程嘗試獲取對象鎖時,如果對象處于無鎖狀態,線程會在自己的棧幀中創建一個鎖記錄(Lock Record),并將對象的Mark Word復制到這個鎖記錄中。然后,線程嘗試通過CAS操作將對象的Mark Word更新為指向該鎖記錄的指針,并將Mark Word的最后兩位設置為00,表示輕量級鎖狀態。
鎖膨脹觸發條件
-
CAS操作失敗:如果在嘗試加輕量級鎖的過程中,CAS操作無法成功,說明有其他線程已經為該對象加了輕量級鎖。此時,當前線程無法通過CAS操作將Mark Word更新為指向自己的鎖記錄,因此CAS操作會失敗。
鎖膨脹過程
-
申請Monitor鎖:當CAS操作失敗后,線程會進入鎖膨脹流程。首先,線程會為對象申請一個Monitor鎖,并將對象的Mark Word更新為指向這個Monitor對象的地址。同時,Mark Word的最后兩位會被設置為10,表示重量級鎖狀態。
-
線程阻塞:由于重量級鎖支持阻塞機制,當前線程會被放入Monitor的EntryList中,并進入BLOCKED狀態,等待其他線程釋放鎖。
解鎖過程
-
解鎖失敗:當持有輕量級鎖的線程(Thread-0)退出同步代碼塊時,它會嘗試通過CAS操作將Mark Word恢復為原來的值(即之前保存在鎖記錄中的值)。然而,由于鎖已經膨脹為重量級鎖,Mark Word的值已經指向Monitor對象,并且最后兩位為10,因此CAS操作會失敗。
-
重量級解鎖流程:當CAS操作失敗后,線程會進入重量級解鎖流程。具體步驟如下:
-
根據Mark Word中的Monitor地址找到對應的Monitor對象。
-
將Monitor對象的Owner字段設置為null,表示沒有線程持有該鎖。
-
喚醒EntryList中所有被阻塞的線程,讓它們有機會競爭鎖。
-
自旋優化 自適應自旋鎖
重量級鎖競爭的時候,還可以使用自旋來進行優化,如果當前線程自旋成功(即這時候持鎖線程已經退出了同步塊,釋放了鎖),這時當前線程就可以避免阻塞。
自旋重試成功的情況
線程 1 (cpu 1 上) | 對象 Mark | 線程 2 (cpu 2 上) |
---|---|---|
- | 10(重量鎖) | - |
訪問同步塊,獲取 monitor | 10(重量鎖)重量鎖指針 | - |
成功(加鎖) | 10(重量鎖)重量鎖指針 | - |
執行同步塊 | 10(重量鎖)重量鎖指針 | 訪問同步塊,獲取 monitor |
執行同步塊 | 10(重量鎖)重量鎖指針 | 自旋重試 |
執行完畢 | 10(重量鎖)重量鎖指針 | 自旋重試 |
成功(解鎖) | 01(無鎖) | 自旋重試 |
- | 10(重量鎖)重量鎖指針 | 成功(加鎖) |
- | 10(重量鎖)重量鎖指針 | 執行同步塊 |
自旋鎖一般會重試 10 次。這是 Java 中自旋鎖的默認設置,由 JVM 內部參數 _spinFreq
控制,表示線程在嘗試獲取鎖失敗后,最多會自旋(即循環嘗試)10 次。如果在這 10 次嘗試中仍未成功獲取鎖,線程將不再自旋,而是進入阻塞狀態,等待鎖被釋放 。
從 JDK 1.7 開始,自旋鎖啟用,并且自旋次數由 JVM 動態決定,稱為“自適應自旋鎖”。這種機制會根據前一次在同一個鎖上的自旋時間和鎖的持有者狀態來調整自旋次數,從而在不同場景下都能達到最佳性能 。
自旋重試失敗的情況
線程 1 (cpu 1 上) | 對象 Mark | 線程 2 (cpu 2 上) |
---|---|---|
- | 10(重量鎖) | - |
訪問同步塊,獲取 monitor | 10(重量鎖)重量鎖指針 | - |
成功(加鎖) | 10(重量鎖)重量鎖指針 | - |
執行同步塊 | 10(重量鎖)重量鎖指針 | 訪問同步塊,獲取 monitor |
執行同步塊 | 10(重量鎖)重量鎖指針 | 自旋重試 |
執行同步塊 | 10(重量鎖)重量鎖指針 | 自旋重試 |
執行同步塊 | 10(重量鎖)重量鎖指針 | 阻塞 |
... | ... | ... |
-
在 Java 6 之后自旋鎖是自適應的,比如對象剛剛的一次自旋操作成功過,那么認為這次自旋成功的可能性會高,就多自旋幾次;反之,就少自旋甚至不自旋,總之,比較智能。
-
自旋會占用 CPU 時間,單核 CPU 自旋就是浪費,多核 CPU 自旋才能發揮優勢。
-
Java 7 之后不能控制是否開啟自旋功能
偏向鎖
Mark Work布局
-
thread:23
:23位用于存儲偏向鎖的線程ID。 -
epoch:2
:2位用于存儲偏向鎖的紀元信息。 -
age:4
:4位用于存儲對象的年齡。 -
biased_lock:1
:1位用于表示是否啟用偏向鎖(1表示已啟用)。 -
01
:2位用于表示鎖的狀態(01表示偏向鎖狀態)。
偏向鎖是Java 6中引入的一種鎖優化機制,旨在進一步減少鎖競爭帶來的性能開銷。它在無鎖競爭的情況下,通過將鎖直接偏向于第一個獲取它的線程,從而避免了輕量級鎖中頻繁的CAS(Compare and Swap)操作。
偏向鎖的引入背景
在輕量級鎖的基礎上,JVM發現大多數情況下鎖并不會被多個線程競爭,尤其是同一個線程多次獲取同一把鎖的場景。為了進一步優化這種無競爭情況下的鎖性能,JVM引入了偏向鎖。偏向鎖的核心思想是:將鎖的持有者(線程)直接記錄在對象頭中,后續訪問該鎖的線程無需再進行CAS操作,從而減少不必要的鎖競爭開銷。
偏向鎖的工作原理
第一次加鎖:當第一個線程訪問同步代碼塊時,JVM會通過CAS操作將該線程的ID寫入對象頭的Mark Word中,并將鎖標志位設置為“101”(表示偏向鎖狀態)。
后續訪問:當同一個線程再次訪問該同步代碼塊時,JVM只需檢查對象頭中的線程ID是否與當前線程一致。如果一致,則直接進入同步代碼塊,無需再進行CAS操作。
鎖重入:如果同一個線程對同一對象多次加鎖,JVM會自動處理鎖的重入,但每次重入時仍然只需判斷線程ID是否匹配,無需執行CAS操作。
輕量級鎖和偏向鎖圖示: