淺析Java中關(guān)鍵詞volatile底層的實(shí)現(xiàn)原理
在 Java 并發(fā)編程中,有 3 個(gè)最常用的關(guān)鍵字:synchronized、ReentrantLock 和 volatile。
雖然 volatile 并不像其他兩個(gè)關(guān)鍵字一樣,能保證線程安全,但 volatile 也是并發(fā)編程中最常見(jiàn)的關(guān)鍵字之一。例如,單例模式、CopyOnWriteArrayList 和 ConcurrentHashMap 中都離不開(kāi) volatile。
那么,問(wèn)題來(lái)了,我們知道 synchronized 底層是通過(guò)監(jiān)視器 Monitor 實(shí)現(xiàn)的,ReentrantLock 底層是通過(guò) AQS 的 CAS 實(shí)現(xiàn)的,那 volatile 的底層是如何實(shí)現(xiàn)的?
1.volatile 作用
在了解 volatile 的底層實(shí)現(xiàn)之前,我們需要先了解 volatile 的作用,因?yàn)?volatile 的底層實(shí)現(xiàn)和它的作用息息相關(guān)。
volatile 作用有兩個(gè):保證內(nèi)存可見(jiàn)性和有序性(禁止指令重排序)。
1.1 內(nèi)存可見(jiàn)性
說(shuō)到內(nèi)存可見(jiàn)性問(wèn)題就不得不提 Java 內(nèi)存模型,Java 內(nèi)存模型(Java Memory Model)簡(jiǎn)稱(chēng)為 JMM,主要是用來(lái)屏蔽不同硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異的,因?yàn)樵诓煌挠布筒煌牟僮飨到y(tǒng)下,內(nèi)存的訪問(wèn)是有一定的差異得,這種差異會(huì)導(dǎo)致相同的代碼在不同的硬件和不同的操作系統(tǒng)下有著不一樣的行為,而 Java 內(nèi)存模型就是解決這個(gè)差異,統(tǒng)一相同代碼在不同硬件和不同操作系統(tǒng)下的差異的。
Java 內(nèi)存模型規(guī)定:所有的變量(實(shí)例變量和靜態(tài)變量)都必須存儲(chǔ)在主內(nèi)存中,每個(gè)線程也會(huì)有自己的工作內(nèi)存,線程的工作內(nèi)存保存了該線程用到的變量和主內(nèi)存的副本拷貝,線程對(duì)變量的操作都在工作內(nèi)存中進(jìn)行。線程不能直接讀寫(xiě)主內(nèi)存中的變量,如下圖所示:

然而,Java 內(nèi)存模型會(huì)帶來(lái)一個(gè)新的問(wèn)題,那就是內(nèi)存可見(jiàn)性問(wèn)題,也就是當(dāng)某個(gè)線程修改了主內(nèi)存中共享變量的值之后,其他線程不能感知到此值被修改了,它會(huì)一直使用自己工作內(nèi)存中的“舊值”,這樣程序的執(zhí)行結(jié)果就不符合我們的預(yù)期了,這就是內(nèi)存可見(jiàn)性問(wèn)題,我們用以下代碼來(lái)演示一下這個(gè)問(wèn)題:
private static boolean flag = false;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (!flag) {
}
System.out.println("終止執(zhí)行");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("設(shè)置 flag=true");
flag = true;
}
});
t2.start();
}
以上代碼我們預(yù)期的結(jié)果是,在線程 1 執(zhí)行了 1s 之后,線程 2 將 flag 變量修改為 true,之后線程 1 終止執(zhí)行,然而,因?yàn)榫€程 1 感知不到 flag 變量發(fā)生了修改,也就是內(nèi)存可見(jiàn)性問(wèn)題,所以會(huì)導(dǎo)致線程 1 會(huì)永遠(yuǎn)的執(zhí)行下去,最終我們看到的結(jié)果是這樣的:

如何解決以上問(wèn)題呢?只需要給變量 flag 加上 volatile 修飾即可,具體的實(shí)現(xiàn)代碼如下:
private volatile static boolean flag = false;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (!flag) {
}
System.out.println("終止執(zhí)行");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("設(shè)置 flag=true");
flag = true;
}
});
t2.start();
}
以上程序的執(zhí)行結(jié)果如下圖所示:

1.2 有序性
有序性也叫做禁止指令重排序。
指令重排序是指編譯器或 CPU 為了優(yōu)化程序的執(zhí)行性能,而對(duì)指令進(jìn)行重新排序的一種手段。
指令重排序的實(shí)現(xiàn)初衷是好的,但是在多線程執(zhí)行中,如果執(zhí)行了指令重排序可能會(huì)導(dǎo)致程序執(zhí)行出錯(cuò)。指令重排序最典型的一個(gè)問(wèn)題就發(fā)生在單例模式中,比如以下問(wèn)題代碼:
public class Singleton {
private Singleton() {}
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) { // ①
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // ②
}
}
}
return instance;
}
}
以上問(wèn)題發(fā)生在代碼 ② 這一行“instance = new Singleton();”,這行代碼看似只是一個(gè)創(chuàng)建對(duì)象的過(guò)程,然而它的實(shí)際執(zhí)行卻分為以下 3 步:
- 創(chuàng)建內(nèi)存空間。
- 在內(nèi)存空間中初始化對(duì)象 Singleton。
- 將內(nèi)存地址賦值給 instance 對(duì)象(執(zhí)行了此步驟,instance 就不等于 null 了)。
如果此變量不加 volatile,那么線程 1 在執(zhí)行到上述代碼的第 ② 處時(shí)就可能會(huì)執(zhí)行指令重排序,將原本是 1、2、3 的執(zhí)行順序,重排為 1、3、2。但是特殊情況下,線程 1 在執(zhí)行完第 3 步之后,如果來(lái)了線程 2 執(zhí)行到上述代碼的第 ① 處,判斷 instance 對(duì)象已經(jīng)不為 null,但此時(shí)線程 1 還未將對(duì)象實(shí)例化完,那么線程 2 將會(huì)得到一個(gè)被實(shí)例化“一半”的對(duì)象,從而導(dǎo)致程序執(zhí)行出錯(cuò),這就是為什么要給私有變量添加 volatile 的原因了。
要使以上單例模式變?yōu)榫€程安全的程序,需要給 instance 變量添加 volatile 修飾,它的最終實(shí)現(xiàn)代碼如下:
public class Singleton {
private Singleton() {}
// 使用 volatile 禁止指令重排序
private static volatile Singleton instance = null; // 【主要是此行代碼發(fā)生了變化】
public static Singleton getInstance() {
if (instance == null) { // ①
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // ②
}
}
}
return instance;
}
}
2.volatile 實(shí)現(xiàn)原理
volatile 實(shí)現(xiàn)原理和它的作用有關(guān),我們首先先來(lái)看它的內(nèi)存可見(jiàn)性。
2.1 內(nèi)存可見(jiàn)性實(shí)現(xiàn)原理
volatile 內(nèi)存可見(jiàn)性主要通過(guò) lock 前綴指令實(shí)現(xiàn)的,它會(huì)鎖定當(dāng)前內(nèi)存區(qū)域的緩存(緩存行),并且立即將當(dāng)前緩存行數(shù)據(jù)寫(xiě)入主內(nèi)存(耗時(shí)非常短),回寫(xiě)主內(nèi)存的時(shí)候會(huì)通過(guò) MESI 協(xié)議使其他線程緩存了該變量的地址失效,從而導(dǎo)致其他線程需要重新去主內(nèi)存中重新讀取數(shù)據(jù)到其工作線程中。
什么 MESI 協(xié)議?
MESI 協(xié)議,全稱(chēng)為 Modified, Exclusive, Shared, Invalid,是一種高速緩存一致性協(xié)議。它是為了解決多處理器(CPU)在并發(fā)環(huán)境下,多個(gè) CPU 緩存不一致問(wèn)題而提出的。MESI 協(xié)議定義了高速緩存中數(shù)據(jù)的四種狀態(tài):
- Modified(M):表示緩存行已經(jīng)被修改,但還沒(méi)有被寫(xiě)回主存儲(chǔ)器。在這種狀態(tài)下,只有一個(gè) CPU 能獨(dú)占這個(gè)修改狀態(tài)。
- Exclusive(E):表示緩存行與主存儲(chǔ)器相同,并且是主存儲(chǔ)器的唯一拷貝。這種狀態(tài)下,只有一個(gè) CPU 能獨(dú)占這個(gè)狀態(tài)。
- Shared(S):表示此高速緩存行可能存儲(chǔ)在計(jì)算機(jī)的其他高速緩存中,并且與主存儲(chǔ)器匹配。在這種狀態(tài)下,各個(gè) CPU 可以并發(fā)的對(duì)這個(gè)數(shù)據(jù)進(jìn)行讀取,但都不能進(jìn)行寫(xiě)操作。
- Invalid(I):表示此緩存行無(wú)效或已過(guò)期,不能使用。
MESI 協(xié)議的主要用途是確保在多個(gè) CPU 共享內(nèi)存時(shí),各個(gè) CPU 的緩存數(shù)據(jù)能夠保持一致性。當(dāng)某個(gè) CPU 對(duì)共享數(shù)據(jù)進(jìn)行修改時(shí),它會(huì)將這個(gè)數(shù)據(jù)的狀態(tài)從 S(共享)或 E(獨(dú)占)狀態(tài)轉(zhuǎn)變?yōu)?M(修改)狀態(tài),并等待適當(dāng)?shù)臅r(shí)機(jī)將這個(gè)修改寫(xiě)回主存儲(chǔ)器。同時(shí),它會(huì)向其他 CPU 廣播一個(gè)“無(wú)效消息”,使得其他 CPU 將自己緩存中對(duì)應(yīng)的數(shù)據(jù)狀態(tài)轉(zhuǎn)變?yōu)镮(無(wú)效)狀態(tài),從而在下次訪問(wèn)這個(gè)數(shù)據(jù)時(shí)能夠從主存儲(chǔ)器或其他 CPU 的緩存中重新獲取正確的數(shù)據(jù)。
這種協(xié)議可以確保在多處理器環(huán)境中,各個(gè) CPU 的緩存數(shù)據(jù)能夠正確、一致地反映主存儲(chǔ)器中的數(shù)據(jù)狀態(tài),從而避免由于緩存不一致導(dǎo)致的數(shù)據(jù)錯(cuò)誤或程序異常。
2.2 有序性實(shí)現(xiàn)原理
volatile 的有序性是通過(guò)插入內(nèi)存屏障(Memory Barrier),在內(nèi)存屏障前后禁止重排序優(yōu)化,以此實(shí)現(xiàn)有序性的。
什么是內(nèi)存屏障?
內(nèi)存屏障(Memory Barrier 或 Memory Fence)是一種硬件級(jí)別的同步操作,它強(qiáng)制處理器按照特定順序執(zhí)行內(nèi)存訪問(wèn)操作,確保內(nèi)存操作的順序性,阻止編譯器和 CPU 對(duì)內(nèi)存操作進(jìn)行不必要的重排序。內(nèi)存屏障可以確??缭狡琳系淖x寫(xiě)操作不會(huì)交叉進(jìn)行,以此維持程序的內(nèi)存一致性模型。
在 Java 內(nèi)存模型(JMM)中,volatile 關(guān)鍵字用于修飾變量時(shí),能夠保證該變量的可見(jiàn)性和有序性。關(guān)于有序性,volatile 通過(guò)內(nèi)存屏障的插入來(lái)實(shí)現(xiàn):
- 寫(xiě)內(nèi)存屏障(Store Barrier / Write Barrier):當(dāng)線程寫(xiě)入 volatile 變量時(shí),JMM 會(huì)在寫(xiě)操作前插入 StoreStore 屏障,確保在這次寫(xiě)操作之前的所有普通寫(xiě)操作都已完成。接著在寫(xiě)操作后插入 StoreLoad 屏障,強(qiáng)制所有后來(lái)的讀寫(xiě)操作都在此次寫(xiě)操作完成之后執(zhí)行,這就確保了其他線程能立即看到 volatile 變量的最新值。
- 讀內(nèi)存屏障(Load Barrier / Read Barrier):當(dāng)線程讀取 volatile 變量時(shí),JMM 會(huì)在讀操作前插入 LoadLoad 屏障,確保在此次讀操作之前的所有讀操作都已完成。而在讀操作后插入 LoadStore 屏障,防止在此次讀操作之后的寫(xiě)操作被重排序到讀操作之前,這樣就確保了對(duì) volatile 變量的讀取總是能看到之前對(duì)同一變量或其他相關(guān)變量的寫(xiě)入結(jié)果。
通過(guò)這種方式,volatile 關(guān)鍵字有效地實(shí)現(xiàn)了內(nèi)存操作的順序性,從而保證了多線程環(huán)境下對(duì) volatile 變量的操作遵循 happens-before 原則,確保了并發(fā)編程的正確性。
2.3 簡(jiǎn)單回答
因?yàn)閮?nèi)存屏障的作用既能保證內(nèi)存可見(jiàn)性,同時(shí)又能禁止指令重排序。因此你也可以籠統(tǒng)的回答 volatile 是通過(guò)內(nèi)存屏障實(shí)現(xiàn)的。但是,回答的越細(xì),面試的成績(jī)?cè)礁?,面試的通過(guò)率也就越高。
課后思考
什么是 happens-before 原則?除了 synchronized、ReentrantLock 和 volatile 之外,并發(fā)編程中還有哪些常見(jiàn)的關(guān)鍵字呢?它們背后的實(shí)現(xiàn)原理又是什么呢?
到此這篇關(guān)于淺析Java中關(guān)鍵詞volatile底層的實(shí)現(xiàn)原理的文章就介紹到這了,更多相關(guān)Java volatile原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解SpringMVC加載配置Properties文件的幾種方式
這篇文章主要介紹了詳解SpringMVC加載配置Properties文件的幾種方式,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-02-02
java數(shù)據(jù)輸出打印流PrintStream和PrintWriter面試精講
這篇文章主要為大家介紹了java數(shù)據(jù)輸出打印流PrintStream和PrintWriter面試精講,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
Java 并發(fā)編程創(chuàng)建線程的四種方式示例詳解
本文對(duì)比Java創(chuàng)建線程的四種方式:繼承Thread類(lèi)(單繼承限制)、實(shí)現(xiàn)Runnable接口(靈活推薦)、實(shí)現(xiàn)Callable接口(支持返回值)、使用線程池(最佳實(shí)踐,資源管理高效),推薦優(yōu)先使用線程池提升系統(tǒng)穩(wěn)定性與資源利用率,感興趣的朋友一起看看吧2025-09-09
Java工作中常見(jiàn)的并發(fā)問(wèn)題處理方法總結(jié)
這篇文章主要介紹了Java工作中常見(jiàn)的并發(fā)問(wèn)題處理方法總結(jié),文章內(nèi)容講解的很清晰,有不太懂得同學(xué)可以跟著學(xué)習(xí)下2021-02-02
Java實(shí)現(xiàn)經(jīng)典游戲復(fù)雜迷宮
這篇文章主要介紹了如何利用java語(yǔ)言實(shí)現(xiàn)經(jīng)典《復(fù)雜迷宮》游戲,文中采用了swing技術(shù)進(jìn)行了界面化處理,感興趣的小伙伴可以動(dòng)手試一試2022-02-02
spring webflux自定義netty 參數(shù)解析
這篇文章主要介紹了spring webflux自定義netty 參數(shù)解析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
詳細(xì)分析Java內(nèi)部類(lèi)——局部?jī)?nèi)部類(lèi)
這篇文章主要介紹了Java局部?jī)?nèi)部類(lèi)的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)Java 內(nèi)部類(lèi)的知識(shí),感興趣的朋友可以了解下2020-08-08
SpringBoot實(shí)現(xiàn)IP限流的示例代碼
在高并發(fā)的互聯(lián)網(wǎng)應(yīng)用中,系統(tǒng)穩(wěn)定性面臨嚴(yán)峻挑戰(zhàn),爬蟲(chóng)、以及不合理的接口調(diào)用都可能導(dǎo)致系統(tǒng)資源耗盡,影響正常用戶(hù)體驗(yàn),為了保障系統(tǒng)的穩(wěn)定性和可用性,本文將深入探討如何在 Spring Boot 中實(shí)現(xiàn) IP 限流,需要的朋友可以參考下2025-06-06
Java 創(chuàng)建兩個(gè)線程模擬對(duì)話并交替輸出實(shí)現(xiàn)解析
這篇文章主要介紹了Java 創(chuàng)建兩個(gè)線程模擬對(duì)話并交替輸出實(shí)現(xiàn)解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10
Spring Security如何使用URL地址進(jìn)行權(quán)限控制
這篇文章主要介紹了Spring Security如何使用URL地址進(jìn)行權(quán)限控制,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12

