Java 內(nèi)存模型 (JMM) 與 volatile 底層實(shí)現(xiàn)
在 Java 并發(fā)編程的江湖里,volatile 是最輕量級(jí)的同步機(jī)制,但也是最容易被誤用、最難講透的一個(gè)關(guān)鍵字。很多開(kāi)發(fā)者能脫口而出“可見(jiàn)性”和“禁止重排序”,但若追問(wèn)其底層驅(qū)動(dòng)力是什么?為什么它不能保證原子性?往往就語(yǔ)焉不詳。
今天,我們就撥開(kāi)迷霧,從硬件底層到 JVM 規(guī)范,一步步徹底講透 JMM 與 volatile。
1. 這篇文章要解決什么問(wèn)題?
在多線(xiàn)程環(huán)境下,我們經(jīng)常會(huì)遇到一些“詭異”的現(xiàn)象:
- 不可見(jiàn)性:線(xiàn)程 A 修改了一個(gè)全局變量,線(xiàn)程 B 卻一直讀到舊值,導(dǎo)致代碼邏輯死循環(huán)。
- 亂序搞鬼:明明代碼寫(xiě)的是先初始化對(duì)象再賦值給引用,結(jié)果線(xiàn)程 B 拿到了一個(gè)還沒(méi)初始化完的“半成品”對(duì)象(經(jīng)典 DCL 漏洞)。
這些現(xiàn)象背后的根源是 CPU 緩存不一致 和 指令重排序。JMM(Java Memory Model)和 volatile 的出現(xiàn),就是為了給開(kāi)發(fā)者提供一套標(biāo)準(zhǔn)的“契約”,確保在多線(xiàn)程環(huán)境下內(nèi)存交互的正確性。
2. 核心原理:為什么需要 JMM?
JMM 的抽象模型
Java 虛擬機(jī)規(guī)范定義了 JMM,目的是屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪(fǎng)問(wèn)差異。
JMM 規(guī)定:
- 主內(nèi)存(Main Memory):所有變量都存儲(chǔ)在主內(nèi)存中。
- 工作內(nèi)存(Working Memory):每個(gè)線(xiàn)程都有自己的工作內(nèi)存,保存了該線(xiàn)程使用到的變量的主內(nèi)存副本。
線(xiàn)程對(duì)變量的所有操作(讀取、賦值)都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫(xiě)主內(nèi)存。

硬件背景:千里尋蹤 MESI 協(xié)議
為什么需要工作內(nèi)存?因?yàn)?CPU 太快了,內(nèi)存太慢了。為了彌補(bǔ)速度差,CPU 引入了多級(jí)緩存(L1/L2/L3)。
當(dāng)多個(gè) CPU 核心同時(shí)操作同一個(gè)內(nèi)存地址時(shí),就會(huì)出現(xiàn)緩存不一致。硬件層面通過(guò) MESI(Modified, Exclusive, Shared, Invalid)協(xié)議 來(lái)解決:
- Modified:該行數(shù)據(jù)被修改,與主存不一致,需寫(xiě)回。
- Exclusive:該行數(shù)據(jù)僅由當(dāng)前 CPU 持有,且與主存一致。
- Shared:多核共享,與主存一致。
- Invalid:該行數(shù)據(jù)失效,需從主存或其它核心重新加載。
volatile 在底層正是利用了觸發(fā)硬件緩存一致性的機(jī)制。
重排序:代碼并不總是按你想的運(yùn)行
為了提高性能,從源代碼到執(zhí)行指令,會(huì)經(jīng)歷三重重排序:
- 編譯器優(yōu)化重排序:編譯器在不改變單線(xiàn)程語(yǔ)義的前提下重排。
- 指令級(jí)并行重排序:CPU 將多條指令重疊執(zhí)行。
- 內(nèi)存系統(tǒng)重排序:由于緩存和讀寫(xiě)緩沖區(qū)的存在,加載和存儲(chǔ)看起來(lái)是亂序的。
3. 流程/機(jī)制描述:volatile 是如何工作的?
內(nèi)存屏障(Memory Barrier)
JVM 會(huì)在 volatile 變量讀寫(xiě)前后插入 內(nèi)存屏障,它是一組處理器指令,用于限制編譯器和處理器的重排序。
JMM 的屏障規(guī)則非常嚴(yán)苛:
- StoreStore 屏障:在
volatile寫(xiě)之前插入,禁止前面的普通寫(xiě)和volatile寫(xiě)重排序。 - StoreLoad 屏障:在
volatile寫(xiě)之后插入,保證該寫(xiě)操作對(duì)所有處理器可見(jiàn)(開(kāi)銷(xiāo)最大,最關(guān)鍵)。 - LoadLoad 屏障:在
volatile讀之后插入,禁止后面所有普通讀操作和該讀重排序。 - LoadStore 屏障:在
volatile讀之后插入,禁止后面所有普通寫(xiě)操作和該讀重排序。

x86 底層:lock 前綴指令
在常見(jiàn)的 x86 架構(gòu) CPU 上,volatile 的底層實(shí)現(xiàn)其實(shí)是依靠一個(gè) lock 前綴指令。 當(dāng) JVM 執(zhí)行帶有 volatile 的寫(xiě)操作時(shí),會(huì)生成的匯編代碼中會(huì)包含 lock addl $0x0, (%esp)(或者類(lèi)似的空操作)。
這個(gè) lock 前綴有兩大核心作用:
- 立即刷新主存:它會(huì)將該 CPU 核緩存行的數(shù)據(jù)立即寫(xiě)回到系統(tǒng)內(nèi)存。
- 使其它緩存失效:由于 MESI 協(xié)議的嗅探機(jī)制,其它 CPU 核心會(huì)監(jiān)聽(tīng)到該數(shù)據(jù)的變化,并將其對(duì)應(yīng)的緩存行設(shè)置為 Invalid 狀態(tài)。下次其它核心讀取時(shí),強(qiáng)制去主存加載。
4. 關(guān)鍵代碼/示例
場(chǎng)景一:可見(jiàn)性演示
如果不用 volatile,這個(gè)程序可能永遠(yuǎn)不會(huì)停止。
import java.util.concurrent.TimeUnit;
/**
* 可見(jiàn)性案例:Flag 標(biāo)記位
*/
public class VisibilityDemo {
// 若不加 volatile,主線(xiàn)程修改 stop 標(biāo)記后,workThread 可能永遠(yuǎn)感知不到
private static volatile boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread workThread = new Thread(() -> {
System.out.println("工作線(xiàn)程啟動(dòng)...");
while (!stop) {
// 循環(huán)執(zhí)行業(yè)務(wù)
}
System.out.println("工作線(xiàn)程感知到停止信號(hào),退出循環(huán)。");
});
workThread.start();
// 睡眠 1 秒確保工作線(xiàn)程已經(jīng)進(jìn)入循環(huán)
TimeUnit.SECONDS.sleep(1);
stop = true;
System.out.println("主線(xiàn)程已修改 stop 標(biāo)記為 true");
}
}場(chǎng)景二:禁止重排序(DCL 單例)
這是 volatile 在企業(yè)級(jí)應(yīng)用中最經(jīng)典的場(chǎng)景。
可通過(guò)高并發(fā)模擬驗(yàn)證,或使用JCStress進(jìn)行驗(yàn)證
/**
* 雙重檢查鎖定(DCL)單例模式
*/
public class Singleton {
// 必須加 volatile,防止指令重排序
private static volatile Singleton instance;
private Singleton() {
// 初始化邏輯
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
/*
* 重點(diǎn):new Singleton() 包含三步:
* 1. 分配內(nèi)存空間
* 2. 執(zhí)行構(gòu)造方法初始化對(duì)象
* 3. 將 instance 指向分配的內(nèi)存空間
*
* 若無(wú) volatile,2 和 3 可能重排序。
* 線(xiàn)程 A 執(zhí)行了 1、3,還未執(zhí)行 2 時(shí),線(xiàn)程 B 判斷 instance 不為空,
* 于是拿到了一個(gè)未完成初始化的“空殼”對(duì)象,造成空指針異常。
*/
instance = new Singleton();
}
}
}
return instance;
}
}5. 常見(jiàn)誤區(qū)
誤區(qū) 1:volatile 保證原子性
絕對(duì)錯(cuò)誤! volatile 只保證可見(jiàn)性和有序性。對(duì)于類(lèi)似 i++ 這種操作(包含:讀取、加一、寫(xiě)回),它無(wú)法保證三步操作的整體原子性。多線(xiàn)程下依然會(huì)出現(xiàn)覆寫(xiě)。 對(duì)策:使用 AtomicInteger 或 synchronized。
誤區(qū) 2:volatile 性能非常差
片面。 volatile 的寫(xiě)操作由于需要插入 StoreLoad 屏障刷新緩存,確實(shí)比普通寫(xiě)慢。但在讀操作上,由于現(xiàn)代 CPU 的優(yōu)化,其開(kāi)銷(xiāo)非常接近普通讀。它比 synchronized 這種重量級(jí)鎖要快得多。
6. 實(shí)際工作中怎么用?
- 狀態(tài)標(biāo)志位:如上面的
stop標(biāo)記,用于優(yōu)雅退出線(xiàn)程。 - 多線(xiàn)程環(huán)境下的單次賦值:如 DCL 單例中防止拿到半初始化對(duì)象。
- “Happens-Before” 傳遞性配合: JMM 規(guī)定,如果你先寫(xiě)一個(gè)
volatile變量,再由另一個(gè)線(xiàn)程讀這個(gè)變量,那么寫(xiě)之前的所有可見(jiàn)修改對(duì)讀之后的線(xiàn)程都是可見(jiàn)的。你可以利用這一點(diǎn),通過(guò)修改一個(gè)volatile變量來(lái)“順帶”發(fā)布一組其它變量。
總結(jié)
volatile 是深入理解 JVM 內(nèi)存模型的入場(chǎng)券。它就像是 CPU 緩存一致性協(xié)議在 Java 層的投影,通過(guò)內(nèi)存屏障和硬件指令,在紛亂的并發(fā)世界中強(qiáng)行劃定了一道名為“確定性”的邊界。
作為資深開(kāi)發(fā)者,理解它不僅是為了寫(xiě)出高性能的代碼,更是為了掌握系統(tǒng)底層的運(yùn)行規(guī)律,在面對(duì)復(fù)雜的并發(fā)難題時(shí),能一眼看穿真相。
到此這篇關(guān)于Java 內(nèi)存模型 (JMM) 與 volatile 底層實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Java 內(nèi)存模型 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java 如何將網(wǎng)絡(luò)資源url轉(zhuǎn)化為File文件
這篇文章主要介紹了Java 如何將網(wǎng)絡(luò)資源url轉(zhuǎn)化為File文件的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
Java8如何使用Lambda表達(dá)式簡(jiǎn)化代碼詳解
這篇文章主要給大家介紹了關(guān)于Java8如何使用Lambda表達(dá)式簡(jiǎn)化的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
Java實(shí)現(xiàn)JS中的escape和UNescape代碼分享
在PHP和Python中都有類(lèi)似JS中的escape和UNescape函數(shù)的功能,那么Java語(yǔ)言中到底有沒(méi)有類(lèi)似的方法呢?本文就來(lái)介紹一下Java實(shí)現(xiàn)JS中的escape和UNescape轉(zhuǎn)碼方法,需要的朋友可以參考下2017-09-09
Java中注解@Async實(shí)現(xiàn)異步及導(dǎo)致失效原因分析
Async注解用于聲明一個(gè)方法是異步的,當(dāng)在方法上加上這個(gè)注解時(shí)將會(huì)在一個(gè)新的線(xiàn)程中執(zhí)行該方法,而不會(huì)阻塞原始線(xiàn)程,這篇文章主要給大家介紹了關(guān)于Java中注解@Async實(shí)現(xiàn)異步及導(dǎo)致失效原因分析的相關(guān)資料,需要的朋友可以參考下2024-07-07
Java基礎(chǔ)知識(shí)之ByteArrayOutputStream流的使用
這篇文章主要介紹了Java基礎(chǔ)知識(shí)之ByteArrayOutputStream流的使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
Java實(shí)現(xiàn)的簡(jiǎn)單字符串反轉(zhuǎn)操作示例
這篇文章主要介紹了Java實(shí)現(xiàn)的簡(jiǎn)單字符串反轉(zhuǎn)操作,結(jié)合實(shí)例形式分別描述了java遍歷逆序輸出以及使用StringBuffer類(lèi)的reverse()方法兩種字符串反轉(zhuǎn)操作技巧,需要的朋友可以參考下2018-08-08
springboot實(shí)現(xiàn)獲取客戶(hù)端IP地址的示例代碼
本文介紹了在SpringBoot中獲取客戶(hù)端IP地址的幾種方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-11-11
Kafka中的producer攔截器與consumer攔截器詳解
這篇文章主要介紹了Kafka中的producer攔截器與consumer攔截器詳解,Producer 的Interceptor使得用戶(hù)在消息發(fā)送前以及Producer回調(diào)邏輯前有機(jī)會(huì)對(duì)消息做 一些定制化需求,比如修改消息等,需要的朋友可以參考下2023-12-12

