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

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

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

