synchronized底層原理之JVM層面的鎖實現(xiàn)細節(jié)與流程
一、前言
在上一篇文章中,我們掌握了其基礎(chǔ)用法、核心特性及適用場景,知道它能解決并發(fā)編程的原子性、可見性、有序性問題。但你是否好奇:同樣是加鎖,synchronized為何能實現(xiàn)“隱式管理”?鎖的狀態(tài)是如何存儲的?線程之間的鎖競爭是如何被JVM調(diào)度的?
本文將深入Java虛擬機(JVM)底層,從“鎖的載體(對象頭)”“鎖的觸發(fā)指令(字節(jié)碼)”“鎖的核心調(diào)度機制(monitor)”三個維度,完整拆解synchronized的實現(xiàn)邏輯,帶你從“會用”進階到“懂原理”,真正理解隱式鎖的本質(zhì)。
二、對象頭與Mark Word
Java中所有對象都可以作為synchronized的鎖對象,這并非偶然——每個Java對象在內(nèi)存中都包含一個“對象頭”結(jié)構(gòu),而對象頭中的“Mark Word”(標(biāo)記字)正是synchronized鎖狀態(tài)的核心存儲載體。簡單來說:synchronized的鎖,本質(zhì)是對對象頭Mark Word的狀態(tài)修改與競爭。
1. Java對象的內(nèi)存布局
在HotSpot虛擬機中,Java對象的內(nèi)存布局分為三部分:
- 對象頭(Header) :存儲對象的核心元數(shù)據(jù),包括鎖狀態(tài)、線程ID、類指針等,是synchronized鎖機制的核心依賴;
- 實例數(shù)據(jù)(Instance Data) :存儲對象的成員變量(包括從父類繼承的變量),是對象的核心業(yè)務(wù)數(shù)據(jù);
- 對齊填充(Padding) :HotSpot虛擬機要求對象內(nèi)存大小必須是8字節(jié)的整數(shù)倍,對齊填充僅用于補全字節(jié)數(shù),無實際業(yè)務(wù)意義。
其中,對象頭是我們關(guān)注的重點,它又分為以下部分:
- Mark Word :占4字節(jié)(32位虛擬機)或8字節(jié)(64位虛擬機),存儲鎖狀態(tài)、偏向線程ID、CAS指針、對象哈希碼、GC分代年齡等信息;
- Klass Pointer(類指針) :占4字節(jié)(32位虛擬機)或8字節(jié)(64位虛擬機,開啟壓縮指針后為4字節(jié)),指向?qū)ο笏鶎兕惖腃lass對象,用于確定對象的類型。
- Array Length(數(shù)組長度):占4字節(jié)(32位虛擬機)或8字節(jié)(64位虛擬機),存儲數(shù)組的元素個數(shù),數(shù)組長度是只有數(shù)組對象才有,普通對象沒有。
2. Mark Word的結(jié)構(gòu)與鎖狀態(tài)關(guān)聯(lián)
Mark Word的結(jié)構(gòu)并非固定不變,而是會根據(jù)對象的“鎖狀態(tài)”動態(tài)變化——JDK1.6為synchronized引入鎖優(yōu)化后,鎖狀態(tài)分為4種:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)、重量級鎖狀態(tài)。不同狀態(tài)下,Mark Word存儲的信息不同,目的是在不同并發(fā)場景下平衡性能與安全性。
以64位HotSpot虛擬機為例,Mark Word的結(jié)構(gòu)如下圖所示:

其中各部分的含義如下:
- lock:2位的鎖狀態(tài)標(biāo)記位,該標(biāo)記的值不同,整個 Mark Word表示的含義不同。biased_lock 和 lock一起,表達的鎖狀態(tài)含義如上圖所示;
- biased_lock:對象是否啟用偏向鎖標(biāo)記,只占1個二進制位。為1時表示對象啟用偏向鎖,為0時表示對象沒有偏向鎖。lock 和 biased_lock共同表示對象處于什么鎖狀態(tài);
- age:4位的Java對象年齡。
- identity_hashcode:31位的對象標(biāo)識hashCode;
- thread:持有偏向鎖的線程ID;
- epoch:偏向鎖的時間戳;
- ptr_to_lock_record:輕量級鎖狀態(tài)下,指向棧中鎖記錄的指針;
- ptr_to_heavyweight_monitor:重量級鎖狀態(tài)下,指向?qū)ο蟊O(jiān)視器 Monitor的指針;
關(guān)鍵說明:
- 鎖狀態(tài)由“偏向鎖標(biāo)志位”和“鎖標(biāo)志位”共同決定(例如:偏向鎖標(biāo)志位1+鎖標(biāo)志位01=偏向鎖;偏向鎖標(biāo)志位0+鎖標(biāo)志位00=輕量級鎖);
- 隨著并發(fā)競爭加劇,鎖狀態(tài)會從“無鎖→偏向鎖→輕量級鎖→重量級鎖”逐步升級,且升級過程不可逆(一旦升級為重量級鎖,無法回退為輕量級鎖或偏向鎖);
- Mark Word是synchronized鎖機制的“核心數(shù)據(jù)結(jié)構(gòu)”,所有鎖的獲取與釋放,本質(zhì)都是對Mark Word中鎖狀態(tài)的修改。
三、鎖的觸發(fā)與釋放
synchronized是“隱式鎖”,其核心優(yōu)勢在于無需手動調(diào)用“lock()”“unlock()”方法——這種隱式管理的實現(xiàn),依賴于JVM在編譯階段為synchronized修飾的代碼插入特定的字節(jié)碼指令。不同用法(修飾方法、修飾代碼塊)對應(yīng)的字節(jié)碼實現(xiàn)略有差異,但核心邏輯一致。
1. 修飾代碼塊:monitorenter與monitorexit指令
當(dāng)synchronized修飾代碼塊時,JVM會在代碼塊的“進入處”插入 monitorenter 指令,在“退出處”(包括正常退出和異常退出)插入 monitorexit 指令。這兩個指令是鎖獲取與釋放的直接觸發(fā)者。
代碼示例與字節(jié)碼分析:
public class SyncBlockDemo {
private final Object lock = new Object();
public void syncBlock() {
// 同步代碼塊
synchronized (lock) {
System.out.println("進入同步代碼塊");
}
}
}使用 javap -v SyncBlockDemo.class 命令反編譯,可得到 syncBlock 方法的字節(jié)碼(核心部分):
public void syncBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #2 // Field lock:Ljava/lang/Object;
4: dup
5: monitorenter // 進入同步代碼塊,獲取鎖(核心指令)
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #4 // String 進入同步代碼塊
11: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: aload_1
15: monitorexit // 正常退出同步代碼塊,釋放鎖(核心指令)
16: goto 24
19: astore_2
20: aload_1
21: monitorexit // 異常退出同步代碼塊,釋放鎖(核心指令)
22: aload_2
23: athrow
24: return
Exception table:
from to target type
6 16 19 any
}字節(jié)碼核心邏輯解讀:
- 0-4行:通過 getfield 指令獲取鎖對象 lock ,并通過 dup 指令復(fù)制一份鎖對象引用(用于后續(xù) monitorenter 和 monitorexit 操作);
- 5行: monitorenter 指令——線程嘗試獲取鎖:若鎖未被持有,則將鎖的持有者設(shè)為當(dāng)前線程,鎖計數(shù)器+1;若鎖已被當(dāng)前線程持有,則鎖計數(shù)器+1(可重入性);若鎖被其他線程持有,則當(dāng)前線程阻塞;
- 6-11行:執(zhí)行同步代碼塊的核心邏輯(打印語句);
- 15行: monitorexit 指令(正常退出)——釋放鎖,鎖計數(shù)器-1,當(dāng)計數(shù)器減為0時,鎖被完全釋放,喚醒等待鎖的線程;
- 19-23行:異常退出分支——JVM為同步代碼塊自動生成異常處理邏輯,確保即使發(fā)生異常,也能通過 monitorexit 釋放鎖,避免鎖泄漏。
關(guān)鍵結(jié)論: monitorenter 和 monitorexit 是synchronized修飾代碼塊的“鎖開關(guān)”,JVM通過這兩個指令實現(xiàn)鎖的獲取與釋放,且異常場景的鎖釋放由JVM自動保障。
2. 修飾方法:ACC_SYNCHRONIZED標(biāo)志位
當(dāng)synchronized修飾實例方法或靜態(tài)方法時,JVM不會插入 monitorenter 和 monitorexit 指令,而是通過在方法的“訪問標(biāo)志(accessflags)”中添加 ACCSYNCHRONIZED 標(biāo)志位來實現(xiàn)鎖機制。
代碼示例與字節(jié)碼分析:
public class SyncMethodDemo {
// 同步實例方法
public synchronized void syncInstanceMethod() {
System.out.println("進入同步實例方法");
}
// 同步靜態(tài)方法
public static synchronized void syncStaticMethod() {
System.out.println("進入同步靜態(tài)方法");
}
}反編譯后,方法的字節(jié)碼核心部分如下:
// 同步實例方法
public synchronized void syncInstanceMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 新增ACC_SYNCHRONIZED標(biāo)志位
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String 進入同步實例方法
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
// 同步靜態(tài)方法
public static synchronized void syncStaticMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED // 新增ACC_SYNCHRONIZED標(biāo)志位
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String 進入同步靜態(tài)方法
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return字節(jié)碼核心邏輯解讀:
- 同步方法的字節(jié)碼中,沒有 monitorenter 和 monitorexit 指令,而是通過 ACC_SYNCHRONIZED 標(biāo)志位標(biāo)識“這是一個同步方法”;
- 當(dāng)線程調(diào)用帶有 ACC_SYNCHRONIZED 標(biāo)志的方法時,JVM會自動嘗試獲取鎖:同步實例方法:鎖對象為當(dāng)前實例(this),獲取鎖的邏輯與 monitorenter 一致;同步靜態(tài)方法:鎖對象為當(dāng)前類的Class對象,獲取鎖的邏輯與 monitorenter 一致;
- 方法執(zhí)行完成(正常return或異常拋出)時,JVM會自動釋放鎖,無需手動處理。
關(guān)鍵結(jié)論:synchronized修飾方法的鎖機制,本質(zhì)是JVM對 ACC_SYNCHRONIZED 標(biāo)志位的解析——將鎖的獲取與釋放邏輯嵌入到方法的調(diào)用與返回流程中,實現(xiàn)隱式管理。
四、核心鎖調(diào)度機制
無論是 monitorenter 指令,還是 ACC_SYNCHRONIZED 標(biāo)志位,最終都依賴于“monitor”對象實現(xiàn)鎖的調(diào)度。monitor是操作系統(tǒng)層面的“互斥量(mutex)”的封裝,是synchronized重量級鎖的核心載體,負責(zé)管理線程的鎖競爭與等待。
1. monitor的本質(zhì)與結(jié)構(gòu)
monitor的本質(zhì)是一個“對象”,在HotSpot虛擬機中,它由C++語言實現(xiàn)(對應(yīng) ObjectMonitor 類)。每個Java對象在創(chuàng)建時,都會關(guān)聯(lián)一個monitor對象(可理解為“對象→monitor”的一對一映射),當(dāng)線程嘗試獲取鎖時,實際上是在競爭monitor的“所有權(quán)”。
ObjectMonitor 的核心結(jié)構(gòu)如下(簡化版):
class ObjectMonitor {
// 持有當(dāng)前monitor的線程(鎖的所有者)
Thread* owner;
// 等待鎖的線程隊列(阻塞狀態(tài))
Queue* EntryList;
// 調(diào)用wait()后等待喚醒的線程隊列
Queue* WaitSet;
// 鎖計數(shù)器(實現(xiàn)可重入性)
int count;
// 遞歸鎖計數(shù)器(記錄重入次數(shù))
int recursions;
};2. 基于monitor的鎖競爭流程
當(dāng)多個線程競爭同一把鎖時,JVM通過monitor的 owner 、 EntryList 、 WaitSet 三個核心組件實現(xiàn)調(diào)度,完整流程如下:
- 初始狀態(tài) :monitor的 owner 為null, count 為0, EntryList 和 WaitSet 為空;
- 線程1獲取鎖 :線程1執(zhí)行 monitorenter 指令(或調(diào)用同步方法),發(fā)現(xiàn)monitor的 owner 為null,直接成為 owner , count 置為1,進入臨界區(qū)執(zhí)行代碼;
- 線程2競爭鎖 :線程2嘗試獲取鎖時,發(fā)現(xiàn)monitor的 owner 已為線程1,無法獲取,被JVM放入 EntryList 隊列,進入阻塞狀態(tài)(BLOCKED);
- 線程1釋放鎖 :線程1執(zhí)行完臨界區(qū)代碼,執(zhí)行 monitorexit 指令(或方法返回), count 減為0,釋放monitor的 owner (置為null),并喚醒 EntryList 中的線程(如線程2);
- 線程2再次競爭 :被喚醒的線程2重新嘗試獲取鎖,若此時monitor的 owner 為null,則成為新的 owner , count 置為1,進入臨界區(qū);若仍有其他線程競爭,則再次進入 EntryList 阻塞;
- 線程調(diào)用wait()方法 :若線程1在持有鎖期間調(diào)用了 wait() 方法,則會釋放monitor的 owner , count 置為0,自身進入 WaitSet 隊列,進入等待狀態(tài)(WAITING);
- 線程被notify()喚醒 :當(dāng)其他線程調(diào)用同一鎖對象的 notify() 或 notifyAll() 方法時,JVM會將 WaitSet 中的線程(如線程1)轉(zhuǎn)移到 EntryList 隊列,等待再次競爭鎖。
3. 重量級鎖的性能瓶頸根源
在JDK1.6之前,synchronized直接使用上述monitor機制(即重量級鎖),導(dǎo)致性能較差。其核心瓶頸在于:
- 線程狀態(tài)切換成本高 :線程從 EntryList 的阻塞狀態(tài)(BLOCKED)被喚醒后,需要從“內(nèi)核態(tài)”切換到“用戶態(tài)”,而狀態(tài)切換涉及操作系統(tǒng)內(nèi)核的調(diào)度,耗時較長;
- 鎖競爭的排他性開銷 :即使只有兩個線程交替競爭鎖,也需要頻繁進行“阻塞→喚醒”的狀態(tài)切換,無法充分利用CPU資源。
這也是JDK1.6引入偏向鎖、輕量級鎖優(yōu)化的核心原因——在低并發(fā)場景下,避免使用重量級鎖的內(nèi)核態(tài)調(diào)度,提升鎖競爭效率。
五、可重入性的底層實現(xiàn)
在上一文中我們提到,synchronized具備可重入性——同一線程可以多次獲取同一把鎖,不會因已持有鎖而阻塞。這一特性的底層實現(xiàn),依賴于monitor的 count (鎖計數(shù)器)和 recursions (遞歸鎖計數(shù)器)。
具體實現(xiàn)邏輯:
- 線程第一次獲取鎖時,monitor的 owner 設(shè)為當(dāng)前線程, count 置為1, recursions 置為0;
- 線程再次獲取同一把鎖時(如同步方法調(diào)用同步方法),JVM檢測到當(dāng)前線程已是monitor的 owner ,則直接將 count 加1, recursions 加1(記錄重入次數(shù));
- 線程每次釋放鎖時, count 減1;
- 當(dāng) count 減為0時,說明線程已完全釋放鎖, owner 置為null,其他線程可競爭。
示例驗證:
public class ReentrantDemo {
public synchronized void methodA() {
System.out.println("進入methodA");
methodB(); // 同一線程調(diào)用同步方法,重入鎖
}
public synchronized void methodB() {
System.out.println("進入methodB");
}
public static void main(String[] args) {
new ReentrantDemo().methodA();
}
}執(zhí)行流程:
- 線程調(diào)用 methodA() ,獲取鎖,monitor的 count=1 ;
- 線程在 methodA() 中調(diào)用 methodB() ,再次獲取同一把鎖,monitor的 count=2 ;
- methodB() 執(zhí)行完成,釋放鎖, count=1 ;
- methodA() 執(zhí)行完成,釋放鎖, count=0 ,鎖完全釋放。
關(guān)鍵結(jié)論:可重入性的核心是“鎖計數(shù)器”——通過計數(shù)記錄線程的重入次數(shù),確保線程只有在完全釋放所有重入鎖后,才會讓出鎖的所有權(quán)。
六、內(nèi)存可見性的底層保障
在上一文中我們提到,synchronized能保證內(nèi)存可見性——一個線程對共享變量的修改,會被后續(xù)獲取同一把鎖的線程及時感知。這一特性的底層實現(xiàn),依賴于JVM在鎖的獲取與釋放過程中插入的“內(nèi)存屏障”。
1. 內(nèi)存屏障的核心作用
內(nèi)存屏障是CPU層面的指令,用于禁止指令重排序,并強制刷新工作內(nèi)存與主內(nèi)存的數(shù)據(jù)。JVM通過插入內(nèi)存屏障,確保:
- 線程對共享變量的修改,必須同步到主內(nèi)存;
- 線程讀取共享變量時,必須從主內(nèi)存加載最新數(shù)據(jù)。
2. synchronized的內(nèi)存屏障插入規(guī)則
JVM為synchronized的鎖獲取與釋放過程,制定了嚴(yán)格的內(nèi)存屏障插入規(guī)則:
- 鎖獲取時 :在 monitorenter 指令(或同步方法調(diào)用)后,插入“LoadLoad屏障”和“LoadStore屏障”:
- LoadLoad屏障:禁止后續(xù)的讀操作與當(dāng)前讀操作重排序;
- LoadStore屏障:禁止后續(xù)的寫操作與當(dāng)前讀操作重排序;
- 核心效果:強制線程從主內(nèi)存加載共享變量的最新數(shù)據(jù),避免讀取舊值。
- 鎖釋放時 :在 monitorexit 指令(或同步方法返回)前,插入“StoreStore屏障”和“StoreLoad屏障”:
- StoreStore屏障:禁止當(dāng)前寫操作與后續(xù)寫操作重排序;
- StoreLoad屏障:禁止當(dāng)前寫操作與后續(xù)讀操作重排序,并強制將工作內(nèi)存中的數(shù)據(jù)同步到主內(nèi)存;
- 核心效果:確保線程對共享變量的修改已同步到主內(nèi)存,讓其他線程可見。
3. 與volatile的可見性機制對比
synchronized與volatile都能保證內(nèi)存可見性,但實現(xiàn)邏輯不同:
特性 | synchronized | volatile |
|---|---|---|
可見性實現(xiàn)方式 | 通過鎖獲取/釋放時插入的內(nèi)存屏障,間接保證可見性 | 直接在寫操作后插入StoreLoad屏障,讀操作前插入LoadLoad屏障,直接保證可見性 |
額外保障 | 同時保證原子性、有序性 | 僅保證可見性、有序性,不保證原子性 |
適用場景 | 臨界區(qū)代碼(多操作組合) | 單個共享變量的讀/寫操作 |
七、總結(jié)
本文從JVM底層視角,完整拆解了synchronized的實現(xiàn)邏輯,核心可總結(jié)為“三個核心載體+一個調(diào)度機制”:
- 鎖的存儲載體 :對象頭的Mark Word,通過動態(tài)修改鎖狀態(tài)(無鎖→偏向鎖→輕量級鎖→重量級鎖)適配不同并發(fā)場景;
- 鎖的觸發(fā)指令 :修飾代碼塊時通過 monitorenter / monitorexit 指令,修飾方法時通過 ACC_SYNCHRONIZED 標(biāo)志位,實現(xiàn)隱式鎖管理;
- 鎖的調(diào)度機制 :基于monitor對象( ObjectMonitor )的 owner 、 EntryList 、 WaitSet 組件,實現(xiàn)線程的鎖競爭與等待喚醒;
- 可見性與可重入性保障 :通過內(nèi)存屏障保證可見性,通過monitor的鎖計數(shù)器實現(xiàn)可重入性。
理解這些底層原理,能幫你更深刻地理解synchronized的性能特性——為何JDK1.6要引入鎖優(yōu)化?為何偏向鎖適合單線程場景?為何高并發(fā)下synchronized性能會下降?這些問題,我們將在第3篇“synchronized鎖優(yōu)化深度解析”中詳細解答。
下一篇文章,我們將聚焦JDK1.6的鎖優(yōu)化機制,深入剖析偏向鎖、輕量級鎖、重量級鎖的實現(xiàn)細節(jié)與升級流程,帶你理解synchronized性能提升的核心邏輯,敬請關(guān)注!
到此這篇關(guān)于synchronized底層原理之JVM層面的鎖實現(xiàn)細節(jié)與流程的文章就介紹到這了,更多相關(guān)synchronized JVM層面內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot詳解整合SpringSecurity實現(xiàn)全過程
Spring Security基于Spring開發(fā),項目中如果使用Springboot作為基礎(chǔ),配合Spring Security做權(quán)限更加方便,而Shiro需要和Spring進行整合開發(fā)。因此作為spring全家桶中的Spring Security在java領(lǐng)域很常用2022-07-07
MyBatis學(xué)習(xí)教程之開發(fā)Dao的方法教程
這篇文章主要給大家介紹了關(guān)于MyBatis開發(fā)Dao的相關(guān)資料,使用Mybatis開發(fā)Dao,通常有兩個方法,即原始Dao開發(fā)方法和Mapper接口開發(fā)方法。文中通過示例代碼介紹的非常詳細,需要的朋友們下面來一起看看吧。2017-07-07

