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