Java并發(fā)編程之關(guān)鍵字volatile的深入解析
前言
volatile是研究Java并發(fā)編程繞不過去的一個關(guān)鍵字,先說結(jié)論:
volatile的作用:
1.保證被修飾變量的可見性
2.保證程序一定程度上的有序性
3.不能保證原子性
下面,我們將從理論以及實(shí)際的案例來逐個解析上面的三個結(jié)論
一、可見性
什么是可見性?
舉個例子,小明和小紅去看電影,剛開始兩個人都還沒買電影票,小紅就先去買了兩張電影票,沒有告訴小明。小明以為小紅沒買,所以也去買了兩張電影票,因?yàn)樗麄冎挥袃蓚€人,所以他們只能用兩張票,這就是小明和小紅他倆電影票的數(shù)量的可見性。
在講解之前,我們簡單的了解一下JVM當(dāng)中運(yùn)行時數(shù)據(jù)區(qū)的結(jié)構(gòu)

堆內(nèi)存:存放的就是對象,所以它也是JVM當(dāng)中內(nèi)存最大的一區(qū)域
線程私有區(qū):線程中的棧會去從堆當(dāng)中獲取變量的值來進(jìn)行操作,正是因?yàn)槭撬接谢模詢蓚€線程之間的數(shù)據(jù)是不會共享的
元空間:存放靜態(tài)變量以及常量還有被虛擬機(jī)加載的類信息
同理,我們可以將小明和小紅看作java當(dāng)中的兩個線程1和2,共有一個變量
public class volatileTest {
public static boolean flag = false;
public static void main(String[] args) {
try {
new Thread(() -> {
System.out.println("線程1開始");
//線程1當(dāng)中取反值,當(dāng)flag為true時才會跳出循環(huán)
while (!flag) {
}
System.out.println("線程1結(jié)束");
}).start();
Thread.sleep(100);
new Thread(() -> {
System.out.println("線程2開始");
//線程2給flag賦值
flag = true;
System.out.println("線程2結(jié)束");
}).start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
該代碼的運(yùn)行結(jié)果如下:

可以很清楚的看到,只有線程2是跑完了的,但是明明線程2已經(jīng)給flag賦值,線程1并沒有停止循環(huán),這就是flag這個變量沒有可見性,導(dǎo)致線程1一直不停止
解決的方法有兩種
第一:讓每個線程空余時間就去堆同步數(shù)據(jù)(顯然不合理)
第二:使用volatile關(guān)鍵字去修飾變量flag
讓我們加上volatile試試:

這回線程1總算是成功停止了,由此我們可得,volatile是可以讓變量具有可見性的。
學(xué)習(xí)編程不能只知道如何去使用,而是要知道原理,這樣才會有更多的薪資
那么volatile的底層是如何實(shí)現(xiàn)的呢?
如上面jvm運(yùn)行數(shù)據(jù)區(qū)的圖所示,所有的變量都是存在了堆當(dāng)中,而每個線程都是拿到他們的副本進(jìn)行計(jì)算和修改,volatile干了啥事呢,如下圖所示

這里我們介紹一個新的概念,叫總線(各位可以把它理解成進(jìn)行連接線程和堆內(nèi)存,在計(jì)算機(jī)的硬件當(dāng)中,也是有總線的,了解的朋友可以把它用相同概念理解一下)。
當(dāng)一個被volatile修飾的變量,在某一個線程當(dāng)中被修改時,總線會監(jiān)聽到這個變動,并且會讓其他線程中的這個變量失效,簡而言之,當(dāng)線程2當(dāng)中堆flag進(jìn)行了修改,則會導(dǎo)致線程1當(dāng)中的flag失效,就是把這個線程1當(dāng)中的flag刪了。當(dāng)線程1中沒有flag了,它會重新去獲取flag,這個時候,就會使我們的變量flag具有了可見性。
現(xiàn)在我們已經(jīng)知道了,volatile的實(shí)行原理,那么它的底層是如何實(shí)現(xiàn)的?
眾所周知,java語言加載時 -> class ->匯編語言 -> 機(jī)器語言,因?yàn)関olatile是個關(guān)鍵字,所以它的底層是一種匯編語法,被volatile修飾的變量其實(shí)就是給它加了個一個lock前綴指令。
也就是說,當(dāng)面試官問到我們,如何手寫一個volatile時,我們可以說在編譯的層面,添加一個lock前綴指令相當(dāng)于一個內(nèi)存屏障,它本身會提供三個功能
1)它會強(qiáng)制堆緩存的修改操作立即寫入主存
2)如果是寫操作,它會導(dǎo)致其他CPU中對應(yīng)的緩存行無效
3)它會確保指令重排序時不會吧其它的指令排到內(nèi)存屏障之前的位置,也不會之前的操作拍到內(nèi)存屏障之后
前面兩點(diǎn)很好理解,并且我們也進(jìn)行了進(jìn)一步的認(rèn)證,第三點(diǎn)可能有朋友不太明白,這就引出了我們下一個論點(diǎn),volatile可以保證一定的有序性
二、有序性
我們看下面三行代碼
int i=1; int j=2; i =i++;
在我們的理解當(dāng)中,程序時自上而下運(yùn)行的,先是第一行,再是第二行等,然而事實(shí)上,jvm可能會對代碼進(jìn)行重排序,比如它可能就會讓上面的這三行代碼變成下面的狀態(tài)
int i=1; i =i++; int j =2;
為什么會進(jìn)行重排序,目的是讓代碼執(zhí)行的速度更快,當(dāng)然它也不是隨便亂排的,排序的規(guī)則是根據(jù)代碼的依賴性進(jìn)行的判斷,簡而言之就是在不影響結(jié)果的情況下進(jìn)行排序,感興趣的朋友可以自行去了解一下
這是java本身對程序保證的有序性,在不影響運(yùn)行結(jié)果的情況下進(jìn)行重排序,但是僅限于單線程的情況下,在多線程的情況中,并不能有效地保證程序的有序性
下圖為手寫的一個單例模式,不做過多的贅述,左邊為代碼,右邊為翻譯的字節(jié)碼文件

通過上圖可以很清晰的看出,new OnlyObject這個操作重點(diǎn)分為了四步,
第一步:創(chuàng)建這個對象
第二步:調(diào)用這個類的構(gòu)造方法
第三步:添加指向(就是從私有線程當(dāng)中執(zhí)行堆)
第四步:加載
由于java對程序的重排序,會使第二步和第三步進(jìn)行調(diào)換位置,在單線程當(dāng)中不會有任何問題,而在多線程當(dāng)中就有問題了
看下圖代碼

當(dāng)線程1已經(jīng)完成添加指向時,在堆當(dāng)中其實(shí)已經(jīng)分配了一個值,但是這時并沒有調(diào)用構(gòu)造方法,所以導(dǎo)致此時這個對象只是一個半成品對象 ,里面并不是我們想要的值。這時線程2走進(jìn)來,他發(fā)現(xiàn)object并不為空,所以直接返回了,此時的程序跟我們的業(yè)務(wù)并不相符,所以我們需要使用volatile來保證我們的有序性。
總結(jié)
到此這篇關(guān)于Java并發(fā)編程之關(guān)鍵字volatile的文章就介紹到這了,更多相關(guān)Java并發(fā)編程關(guān)鍵字volatile內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringMVC實(shí)戰(zhàn)案例RESTFul實(shí)現(xiàn)添加功能
這篇文章主要為大家介紹了SpringMVC實(shí)戰(zhàn)案例RESTFul實(shí)現(xiàn)添加功能詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05
Java實(shí)戰(zhàn)權(quán)限管理系統(tǒng)的實(shí)現(xiàn)流程
讀萬卷書不如行萬里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SpringBoot+MyBatis+AOP+LayUI+Mysql實(shí)現(xiàn)一個權(quán)限管理系統(tǒng),大家可以在過程中查缺補(bǔ)漏,提升水平2022-01-01
SpringBoot整合Canal+RabbitMQ監(jiān)聽數(shù)據(jù)變更詳解
在現(xiàn)代分布式系統(tǒng)中,實(shí)時獲取數(shù)據(jù)庫的變更信息是一個常見的需求,本文將介紹SpringBoot如何通過整合Canal和RabbitMQ監(jiān)聽數(shù)據(jù)變更,需要的可以參考下2024-12-12
詳解Mybatis-plus(MP)中CRUD操作保姆級筆記
本文主要介紹了Mybatis-plus(MP)中CRUD操作保姆級筆記,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11
springboot項(xiàng)目如何引用公共模塊的bean
這篇文章主要介紹了springboot項(xiàng)目如何引用公共模塊的bean問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08
springboot Mongodb的集成與使用實(shí)例詳解
這篇文章主要介紹了springboot Mongodb的集成與使用實(shí)例詳解,需要的朋友可以參考下2018-04-04

