深入解析Java中volatile的底層原理
一、前言
之前我們學(xué)習(xí)過synchronized
,知道它是一個(gè)重量級的鎖,雖然jdk1.6對其做了很大的優(yōu)化,但是成本還是較高。因此Java另一個(gè)關(guān)鍵字閃亮登場——volatile
。volatile又被稱為輕量級的synchronized,它在多處理器中保證了共享變量的可見性。volatile變量修飾符如果使用恰當(dāng)?shù)脑?,它比synchronized的使用和執(zhí)行成本會(huì)更低。下面我們將深入剖析volatile的實(shí)現(xiàn)原理。
二、什么是volatile
Java語言規(guī)范第3版中對volatile的定義如下
Java編程語言允許線程訪問共享變量,為了確保共享變量能被準(zhǔn)確和一致地更新,線程應(yīng)該確保通過排他鎖單獨(dú)獲得這個(gè)變量。Java語言提供了volatile,在某些情況下比鎖要更加方便。如果一個(gè)字段被聲明成volatile,Java線程內(nèi)存模型確保所有線程看到這個(gè)變量的值是一致的。
三、volatile的語義
當(dāng)一個(gè)共享變量被volatile
修飾之后,這個(gè)變量就具備了兩層語義:
- 保證共享變量的可見性:保證了不同線程對這個(gè)變量進(jìn)行操作時(shí)的可見性,即一個(gè)線程修改了某個(gè)變量的值,這新值對其他線程來說是立即可見的。
- 防止局部指令重排序:happens-before規(guī)則中的volatile變量規(guī)則規(guī)定了一個(gè)線程先去寫一個(gè)volatile變量,然后一個(gè)線程去讀這個(gè)變量,那么這個(gè)寫操作的結(jié)果一定對讀的這個(gè)線程可見。
四、volatile的使用
1. volatile保證共享變量的可見性
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ì)有問題,線程2能夠正常結(jié)束線程1的執(zhí)行,但是上述的代碼會(huì)一直死循環(huán)無法結(jié)束。
這是因?yàn)榫€程1先執(zhí)行,此時(shí)stop的值初始為false,線程1執(zhí)行死循環(huán)。根據(jù)Java內(nèi)存模型,每個(gè)線程不能直接訪問主內(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ì)被置為無效
因此當(dāng)線程2修改stop的值的時(shí)候,會(huì)立即寫回主內(nèi)存,同時(shí)使線程1的工作內(nèi)存中的stop變量置為無效。線程1在每次循環(huán)讀取stop的時(shí)候發(fā)現(xiàn)自己的stop變量無效了,會(huì)重新去主內(nèi)存讀取最新的stop的值,即讀取到線程2修改過的最新值,程序正常結(jié)束。
2. volatile保證一定程度的有序性
//線程1: context = loadContext(); //語句1 inited = true; //語句2 //線程2: while(!inited ){ sleep() } doSomethingwithconfig(context);
借用網(wǎng)上的一個(gè)經(jīng)典案例,線程1負(fù)責(zé)初始化上下文,線程2則是線程1初始化結(jié)束后進(jìn)行的后續(xù)操作。我們知道處理器為了提高程序運(yùn)行效率,可能會(huì)對輸入代碼進(jìn)行重排優(yōu)化,其中語句1和語句2不存在依賴關(guān)系,因此處理器可能會(huì)將語句1和語句2進(jìn)行調(diào)換順序,即先執(zhí)行語句2再執(zhí)行語句1,那么如果線程1先執(zhí)行完語句2,還沒有來得及執(zhí)行語句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語句前后的重排序,從而保證了線程安全性。
五、volatile的實(shí)現(xiàn)機(jī)制和原理
通過前面對volatile的語義的使用的介紹,相信大家已經(jīng)有了一個(gè)初步的了解,但是volatile為什么會(huì)實(shí)現(xiàn)這些特性呢?接來下我們就正式進(jìn)入volatile底層原理的講解。
1. 內(nèi)存屏障
在介紹原理前,我們先了解一下內(nèi)存屏障。
1.1 什么是內(nèi)存屏障
- 內(nèi)存屏障(memory barrier)是一個(gè)CPU指令。這條指令可以確保一些特定指令的執(zhí)行順序,影響一些數(shù)據(jù)的可見性(可能是某些指令執(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ù)被訪問前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
- StoreStore屏障
Store1; StoreStore屏障; Store2;
Store1
和 Store2
代表兩條寫入指令。在Store2寫入執(zhí)行前,保證Store1的寫入操作對其它處理器可見。
- LoadStore屏障
Load1; LoadStore屏障; Store2;
在Store2被寫入前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
- StoreLoad屏障
Store1; StoreLoad屏障; Load2;
在Load2讀取操作執(zhí)行前,保證Store1的寫入對所有處理器可見。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)制將對緩存的修改操作立即寫入主存,利用緩存一致性機(jī)制,并且緩存一致性機(jī)制會(huì)阻止同時(shí)修改由兩個(gè)以上CPU緩存的內(nèi)存區(qū)域數(shù)據(jù)。
如果是寫操作,它會(huì)導(dǎo)致其他CPU中對應(yīng)的緩存行無效。
volatile
的底層實(shí)現(xiàn)是通過插入內(nèi)存屏障,但是對于編譯器來說,發(fā)現(xiàn)一個(gè)最優(yōu)布置來最小化插入內(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ī)制,我們來分析volatile是如何實(shí)現(xiàn)可見性和有序性的
3.1 可見性
如果對聲明了volatile變量進(jìn)行寫操作時(shí),JVM會(huì)向處理器發(fā)送一條Lock前綴的指令,將這個(gè)變量所在緩存行的數(shù)據(jù)寫會(huì)到系統(tǒng)內(nèi)存。這一步確保了如果有其他線程對聲明了volatile
變量進(jìn)行修改,則立即更新主內(nèi)存中數(shù)據(jù)。但這時(shí)候其他處理器的緩存還是舊的,所以在多處理器環(huán)境下,為了保證各個(gè)處理器緩存一致,每個(gè)處理會(huì)通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己的緩存是否過期,當(dāng)處理器發(fā)現(xiàn)自己緩存行對應(yīng)的內(nèi)存地址被修改了,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無效狀態(tài),當(dāng)處理器要對這個(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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決SpringBoot集成Eureka導(dǎo)致返回結(jié)果由json變?yōu)閤ml的問題
這篇文章主要介紹了解決SpringBoot集成Eureka導(dǎo)致返回結(jié)果由json變?yōu)閤ml的問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07SpringBoot2.1 RESTful API項(xiàng)目腳手架(種子)項(xiàng)目
這篇文章主要介紹了SpringBoot2.1 RESTful API項(xiàng)目腳手架(種子)項(xiàng)目,用于搭建RESTful API工程的腳手架,只需三分鐘你就可以開始編寫業(yè)務(wù)代碼,不再煩惱于構(gòu)建項(xiàng)目與風(fēng)格統(tǒng)一,感興趣的小伙伴們可以參考一下2018-12-12SpringBoot?項(xiàng)目打成?jar后加載外部配置文件的操作方法
這篇文章主要介紹了SpringBoot?項(xiàng)目打成?jar后加載外部配置文件的操作方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03springboot接口服務(wù),防刷、防止請求攻擊,AOP實(shí)現(xiàn)方式
本文介紹了如何使用AOP防止Spring?Boot接口服務(wù)被網(wǎng)絡(luò)攻擊,通過在pom.xml中加入AOP依賴,創(chuàng)建自定義注解類和AOP切面,以及在業(yè)務(wù)類中使用這些注解,可以有效地對接口進(jìn)行保護(hù),測試表明,這種方法有效地防止了網(wǎng)絡(luò)攻擊2024-11-11Java Springboot之Spring家族的技術(shù)體系
今天帶大家來學(xué)習(xí)Spring家族的技術(shù)體系,文中有非常詳細(xì)的圖文介紹及代碼示例,對正在學(xué)習(xí)java的小伙伴們很有幫助,需要的朋友可以參考下2021-05-05Eclipse插件開發(fā)實(shí)現(xiàn)控制臺(tái)輸出信息的方法
今天小編就為大家分享一篇關(guān)于Eclipse插件開發(fā)實(shí)現(xiàn)控制臺(tái)輸出信息的方法,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-01-01springboot項(xiàng)目如何設(shè)置session的過期時(shí)間
這篇文章主要介紹了springboot項(xiàng)目如何設(shè)置session的過期時(shí)間,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01Java 常見異常(Runtime Exception )詳細(xì)介紹并總結(jié)
這篇文章主要介紹了Java 常見異常(Runtime Exception )詳細(xì)介紹并相關(guān)資料,大家在開發(fā)Java 應(yīng)用軟件的時(shí)候經(jīng)常會(huì)遇到各種異常這里幫大家整理了一部分,并解釋如何解決,需要的朋友可以參考下2016-10-10