Java并發(fā)編程之關(guān)鍵字volatile的深入解析
前言
volatile是研究Java并發(fā)編程繞不過去的一個關(guān)鍵字,先說結(jié)論:
volatile的作用:
1.保證被修飾變量的可見性
2.保證程序一定程度上的有序性
3.不能保證原子性
下面,我們將從理論以及實(shí)際的案例來逐個解析上面的三個結(jié)論
一、可見性
什么是可見性?
舉個例子,小明和小紅去看電影,剛開始兩個人都還沒買電影票,小紅就先去買了兩張電影票,沒有告訴小明。小明以為小紅沒買,所以也去買了兩張電影票,因為他們只有兩個人,所以他們只能用兩張票,這就是小明和小紅他倆電影票的數(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)行操作,正是因為是私有化的,所以兩個線程之間的數(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)行計算和修改,volatile干了啥事呢,如下圖所示
這里我們介紹一個新的概念,叫總線(各位可以把它理解成進(jìn)行連接線程和堆內(nèi)存,在計算機(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ī)器語言,因為volatile是個關(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)文章
java javax.annotation.Resource注解的詳解
這篇文章主要介紹了javax.annotation.Resource注解的詳解的相關(guān)資料,需要的朋友可以參考下2016-10-10restemplate請求亂碼之content-encoding=“gzip“示例詳解
RestTemplate從Spring3.0開始支持的一個HTTP請求工具,它提供了常見的REST請求方案的模板,及一些通用的請求執(zhí)行方法 exchange 以及 execute,接下來通過本文給大家介紹restemplate請求亂碼之content-encoding=“gzip“,需要的朋友可以參考下2024-03-03Spring?Aop+Redis實(shí)現(xiàn)優(yōu)雅記錄接口調(diào)用情況
通常情況下,開發(fā)完一個接口,無論是在測試階段還是生產(chǎn)上線,我們都需要對接口的執(zhí)行情況做一個監(jiān)控,所以本文為大家整理了Spring統(tǒng)計接口調(diào)用的多種方法,希望對大家有所幫助2023-06-06SpringBoot在一定時間內(nèi)限制接口請求次數(shù)的實(shí)現(xiàn)示例
在項目中,接口的暴露在外面,很多人就會惡意多次快速請求,本文主要介紹了SpringBoot在一定時間內(nèi)限制接口請求次數(shù)的實(shí)現(xiàn)示例,具有一定的參考價值,感興趣的可以了解一下2022-03-03Spring Boot讀取resources目錄文件方法詳解
這篇文章主要介紹了Spring Boot讀取resources目錄文件方法詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-01-01Java網(wǎng)絡(luò)編程TCP實(shí)現(xiàn)文件上傳功能
這篇文章主要為大家詳細(xì)介紹了Java網(wǎng)絡(luò)編程TCP實(shí)現(xiàn)文件上傳功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-07-07