深入解析Java中volatile的底層原理
一、前言
之前我們學(xué)習(xí)過(guò)synchronized,知道它是一個(gè)重量級(jí)的鎖,雖然jdk1.6對(duì)其做了很大的優(yōu)化,但是成本還是較高。因此Java另一個(gè)關(guān)鍵字閃亮登場(chǎng)——volatile。volatile又被稱為輕量級(jí)的synchronized,它在多處理器中保證了共享變量的可見(jiàn)性。volatile變量修飾符如果使用恰當(dāng)?shù)脑?,它比synchronized的使用和執(zhí)行成本會(huì)更低。下面我們將深入剖析volatile的實(shí)現(xiàn)原理。
二、什么是volatile
Java語(yǔ)言規(guī)范第3版中對(duì)volatile的定義如下
Java編程語(yǔ)言允許線程訪問(wèn)共享變量,為了確保共享變量能被準(zhǔn)確和一致地更新,線程應(yīng)該確保通過(guò)排他鎖單獨(dú)獲得這個(gè)變量。Java語(yǔ)言提供了volatile,在某些情況下比鎖要更加方便。如果一個(gè)字段被聲明成volatile,Java線程內(nèi)存模型確保所有線程看到這個(gè)變量的值是一致的。
三、volatile的語(yǔ)義
當(dāng)一個(gè)共享變量被volatile修飾之后,這個(gè)變量就具備了兩層語(yǔ)義:
- 保證共享變量的可見(jiàn)性:保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見(jiàn)性,即一個(gè)線程修改了某個(gè)變量的值,這新值對(duì)其他線程來(lái)說(shuō)是立即可見(jiàn)的。
- 防止局部指令重排序:happens-before規(guī)則中的volatile變量規(guī)則規(guī)定了一個(gè)線程先去寫一個(gè)volatile變量,然后一個(gè)線程去讀這個(gè)變量,那么這個(gè)寫操作的結(jié)果一定對(duì)讀的這個(gè)線程可見(jiàn)。
四、volatile的使用
1. volatile保證共享變量的可見(jiàn)性
public class MyTest {
public static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
//線程1
Thread thread1 = new Thread(() -> {
while (!stop) {
//do something...
}
});
//線程2
Thread thread2 = new Thread(() -> doStop());
thread1.start();
//這里的睡眠是保證線程1先執(zhí)行
Thread.sleep(1000);
thread2.start();
}
public static void doStop() {
stop = true;
}
}如上所示的代碼,主要用于線程2去利用共享變量stop去中斷線程1的執(zhí)行。正常情況下如果線程2先執(zhí)行,線程1再執(zhí)行不會(huì)有問(wèn)題,線程2能夠正常結(jié)束線程1的執(zhí)行,但是上述的代碼會(huì)一直死循環(huán)無(wú)法結(jié)束。
這是因?yàn)榫€程1先執(zhí)行,此時(shí)stop的值初始為false,線程1執(zhí)行死循環(huán)。根據(jù)Java內(nèi)存模型,每個(gè)線程不能直接訪問(wèn)主內(nèi)存的,需另拷貝一份到自己的工作內(nèi)存中。因此線程1執(zhí)行的時(shí)候會(huì)拷貝stop的副本到自己的工作空間內(nèi),而線程2執(zhí)行的時(shí)候雖然修改了stop的值,但是線程1感知不到,因此會(huì)一直死循環(huán)。
因此我們可以使用volatile關(guān)鍵字修改stop變量
public static volatile boolean stop =false;
這里的volatile有兩個(gè)作用
- 被volatile關(guān)鍵字修飾的變量修改時(shí)會(huì)立即寫入主內(nèi)存
- 被volatile關(guān)鍵字修飾的變量修改時(shí),其余線程存放的該變量的副本會(huì)被置為無(wú)效
因此當(dāng)線程2修改stop的值的時(shí)候,會(huì)立即寫回主內(nèi)存,同時(shí)使線程1的工作內(nèi)存中的stop變量置為無(wú)效。線程1在每次循環(huán)讀取stop的時(shí)候發(fā)現(xiàn)自己的stop變量無(wú)效了,會(huì)重新去主內(nèi)存讀取最新的stop的值,即讀取到線程2修改過(guò)的最新值,程序正常結(jié)束。
2. volatile保證一定程度的有序性
//線程1:
context = loadContext(); //語(yǔ)句1
inited = true; //語(yǔ)句2
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);借用網(wǎng)上的一個(gè)經(jīng)典案例,線程1負(fù)責(zé)初始化上下文,線程2則是線程1初始化結(jié)束后進(jìn)行的后續(xù)操作。我們知道處理器為了提高程序運(yùn)行效率,可能會(huì)對(duì)輸入代碼進(jìn)行重排優(yōu)化,其中語(yǔ)句1和語(yǔ)句2不存在依賴關(guān)系,因此處理器可能會(huì)將語(yǔ)句1和語(yǔ)句2進(jìn)行調(diào)換順序,即先執(zhí)行語(yǔ)句2再執(zhí)行語(yǔ)句1,那么如果線程1先執(zhí)行完語(yǔ)句2,還沒(méi)有來(lái)得及執(zhí)行語(yǔ)句1的時(shí)候,線程開始執(zhí)行,發(fā)現(xiàn)inited已經(jīng)為true,它就會(huì)認(rèn)為上下文已經(jīng)被初始化完成了,從而會(huì)調(diào)用doSomethingwithconfig方法,此時(shí)就會(huì)發(fā)生錯(cuò)誤。因此我們可以使用volatile關(guān)鍵字修改inited變量,這樣會(huì)禁止inited語(yǔ)句前后的重排序,從而保證了線程安全性。
五、volatile的實(shí)現(xiàn)機(jī)制和原理
通過(guò)前面對(duì)volatile的語(yǔ)義的使用的介紹,相信大家已經(jīng)有了一個(gè)初步的了解,但是volatile為什么會(huì)實(shí)現(xiàn)這些特性呢?接來(lái)下我們就正式進(jìn)入volatile底層原理的講解。
1. 內(nèi)存屏障
在介紹原理前,我們先了解一下內(nèi)存屏障。
1.1 什么是內(nèi)存屏障
- 內(nèi)存屏障(memory barrier)是一個(gè)CPU指令。這條指令可以確保一些特定指令的執(zhí)行順序,影響一些數(shù)據(jù)的可見(jiàn)性(可能是某些指令執(zhí)行后的結(jié)果)。
- 插入一個(gè)內(nèi)存屏障,相當(dāng)于告訴CPU和編譯器先于這個(gè)命令的必須先執(zhí)行,后于這個(gè)命令的必須后執(zhí)行。內(nèi)存屏障另一個(gè)作用是強(qiáng)制更新一次不同CPU的緩存。
1.2 內(nèi)存屏障的分類
- LoadLoad屏障
Load1; LoadLoad屏障; Load2;
Load1 和 Load2 代表兩條讀取指令。在Load2要讀取的數(shù)據(jù)被訪問(wèn)前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
- StoreStore屏障
Store1; StoreStore屏障; Store2;
Store1 和 Store2代表兩條寫入指令。在Store2寫入執(zhí)行前,保證Store1的寫入操作對(duì)其它處理器可見(jiàn)。
- LoadStore屏障
Load1; LoadStore屏障; Store2;
在Store2被寫入前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
- StoreLoad屏障
Store1; StoreLoad屏障; Load2;
在Load2讀取操作執(zhí)行前,保證Store1的寫入對(duì)所有處理器可見(jiàn)。StoreLoad屏障的開銷是四種屏障中最大的。
2. 實(shí)現(xiàn)機(jī)制
從代碼的層面我們看不到volatile的實(shí)現(xiàn)機(jī)制,因此我們需要從匯編指令的層次進(jìn)行研究,我們查看一下第四章第一節(jié)代碼中doStop方法的匯編指令
# 可以看到此時(shí)有一個(gè)lock前綴指令
0x0000000003226f6e: lock add dword ptr [rsp],0h ;*putstatic stop
; - com.jicl.MyTest::doStop@1 (line 35)lock前綴指令實(shí)際上相當(dāng)于一個(gè)內(nèi)存屏障(也成內(nèi)存柵欄),它會(huì)提供3個(gè)功能:
確保指令重排序時(shí)不會(huì)把其后面的指令排到內(nèi)存屏障之前的位置,也不會(huì)把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時(shí),在它前面的操作已經(jīng)全部完成;
強(qiáng)制將對(duì)緩存的修改操作立即寫入主存,利用緩存一致性機(jī)制,并且緩存一致性機(jī)制會(huì)阻止同時(shí)修改由兩個(gè)以上CPU緩存的內(nèi)存區(qū)域數(shù)據(jù)。
如果是寫操作,它會(huì)導(dǎo)致其他CPU中對(duì)應(yīng)的緩存行無(wú)效。
volatile的底層實(shí)現(xiàn)是通過(guò)插入內(nèi)存屏障,但是對(duì)于編譯器來(lái)說(shuō),發(fā)現(xiàn)一個(gè)最優(yōu)布置來(lái)最小化插入內(nèi)存屏障的總數(shù)幾乎是不可能的,所以,JMM采用了保守策略。如下:
- 在每一個(gè)volatile寫操作前面插入一個(gè)StoreStore屏障:保證在volatile寫之前,其前面的所有普通寫操作都已經(jīng)刷新到主內(nèi)存中
- 在每一個(gè)volatile寫操作后面插入一個(gè)StoreLoad屏障:避免volatile寫與后面可能有的volatile讀/寫操作重排序
- 在每一個(gè)volatile讀操作后面插入一個(gè)LoadLoad屏障:禁止處理器把上面的volatile讀與下面的普通讀重排序
- 在每一個(gè)volatile讀操作后面插入一個(gè)LoadStore屏障:禁止處理器把上面的volatile讀與下面的普通寫重排序
3. 實(shí)現(xiàn)原理
在介紹完volatile的底層實(shí)現(xiàn)機(jī)制,我們來(lái)分析volatile是如何實(shí)現(xiàn)可見(jiàn)性和有序性的
3.1 可見(jiàn)性
如果對(duì)聲明了volatile變量進(jìn)行寫操作時(shí),JVM會(huì)向處理器發(fā)送一條Lock前綴的指令,將這個(gè)變量所在緩存行的數(shù)據(jù)寫會(huì)到系統(tǒng)內(nèi)存。這一步確保了如果有其他線程對(duì)聲明了volatile變量進(jìn)行修改,則立即更新主內(nèi)存中數(shù)據(jù)。但這時(shí)候其他處理器的緩存還是舊的,所以在多處理器環(huán)境下,為了保證各個(gè)處理器緩存一致,每個(gè)處理會(huì)通過(guò)嗅探在總線上傳播的數(shù)據(jù)來(lái)檢查自己的緩存是否過(guò)期,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改了,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無(wú)效狀態(tài),當(dāng)處理器要對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作時(shí),會(huì)強(qiáng)制重新從系統(tǒng)內(nèi)存把數(shù)據(jù)讀到處理器緩存里。 這一步確保了其他線程獲得的聲明了volatile變量都是從主內(nèi)存中獲取最新的。
3.2 有序性
Lock前綴指令實(shí)際上相當(dāng)于一個(gè)內(nèi)存屏障(也成內(nèi)存柵欄),它確保指令重排序時(shí)不會(huì)把其后面的指令排到內(nèi)存屏障之前的位置,也不會(huì)把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時(shí),在它前面的操作已經(jīng)全部完成。
到此這篇關(guān)于深入解析Java中volatile的底層原理的文章就介紹到這了,更多相關(guān)volatile的底層原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺析SpringBoot中過(guò)濾器配置與應(yīng)用場(chǎng)景
過(guò)濾器是 Web 三大組件之一,也是項(xiàng)目中常用的工具,這篇文章主要介紹一下過(guò)濾器的概念及在 Spring Boot 中的常用使用方法,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-05-05
SpringBoot集成MyBatis中SQL攔截器的實(shí)戰(zhàn)指南
這篇文章主要為大家詳細(xì)介紹了SpringBoot集成MyBatis中SQL攔截器的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-07-07
SpringBoot生成PDF的五種實(shí)現(xiàn)方法總結(jié)
這篇文章主要介紹了SpringBoot生成PDF的五種實(shí)現(xiàn)方法,在開發(fā)中經(jīng)常會(huì)遇到需要進(jìn)行對(duì)一些數(shù)據(jù)進(jìn)行動(dòng)態(tài)導(dǎo)出PDF文件,然后讓用戶自己選擇是否需要打印出來(lái),這篇文章我們來(lái)介紹五種實(shí)現(xiàn)方法,需要的朋友可以參考下2024-10-10
Springmvc中的轉(zhuǎn)發(fā)重定向和攔截器的示例
本篇文章主要介紹了Springmvc中的轉(zhuǎn)發(fā)重定向和攔截器的示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05
關(guān)于@Autowired的使用及注意事項(xiàng)
這篇文章主要介紹了關(guān)于@Autowired的使用及注意事項(xiàng),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05
Java的Spring框架中實(shí)現(xiàn)發(fā)送郵件功能的核心代碼示例
這篇文章主要介紹了Java的Spring框架中實(shí)現(xiàn)發(fā)送郵件功能的核心代碼示例,包括發(fā)送帶附件的郵件功能的實(shí)現(xiàn),需要的朋友可以參考下2016-03-03
java?JIT調(diào)優(yōu)的實(shí)現(xiàn)
JIT編譯器調(diào)優(yōu)方法包括啟用JIT日志、優(yōu)化熱點(diǎn)代碼、循環(huán)展開、內(nèi)聯(lián)優(yōu)化、逃逸分析以及使用性能分析工具等,本文主要介紹了java?JIT調(diào)優(yōu)的實(shí)現(xiàn),感興趣的可以了解一下2025-02-02

