Java并發(fā)編程之關(guān)鍵字volatile的深入解析
前言
volatile是研究Java并發(fā)編程繞不過(guò)去的一個(gè)關(guān)鍵字,先說(shuō)結(jié)論:
volatile的作用:
1.保證被修飾變量的可見(jiàn)性
2.保證程序一定程度上的有序性
3.不能保證原子性
下面,我們將從理論以及實(shí)際的案例來(lái)逐個(gè)解析上面的三個(gè)結(jié)論
一、可見(jiàn)性
什么是可見(jiàn)性?
舉個(gè)例子,小明和小紅去看電影,剛開(kāi)始兩個(gè)人都還沒(méi)買電影票,小紅就先去買了兩張電影票,沒(méi)有告訴小明。小明以為小紅沒(méi)買,所以也去買了兩張電影票,因?yàn)樗麄冎挥袃蓚€(gè)人,所以他們只能用兩張票,這就是小明和小紅他倆電影票的數(shù)量的可見(jiàn)性。
在講解之前,我們簡(jiǎn)單的了解一下JVM當(dāng)中運(yùn)行時(shí)數(shù)據(jù)區(qū)的結(jié)構(gòu)
堆內(nèi)存:存放的就是對(duì)象,所以它也是JVM當(dāng)中內(nèi)存最大的一區(qū)域
線程私有區(qū):線程中的棧會(huì)去從堆當(dāng)中獲取變量的值來(lái)進(jìn)行操作,正是因?yàn)槭撬接谢模詢蓚€(gè)線程之間的數(shù)據(jù)是不會(huì)共享的
元空間:存放靜態(tài)變量以及常量還有被虛擬機(jī)加載的類信息
同理,我們可以將小明和小紅看作java當(dāng)中的兩個(gè)線程1和2,共有一個(gè)變量
public class volatileTest { public static boolean flag = false; public static void main(String[] args) { try { new Thread(() -> { System.out.println("線程1開(kāi)始"); //線程1當(dāng)中取反值,當(dāng)flag為true時(shí)才會(huì)跳出循環(huán) while (!flag) { } System.out.println("線程1結(jié)束"); }).start(); Thread.sleep(100); new Thread(() -> { System.out.println("線程2開(kāi)始"); //線程2給flag賦值 flag = true; System.out.println("線程2結(jié)束"); }).start(); } catch (Exception e) { e.printStackTrace(); } } }
該代碼的運(yùn)行結(jié)果如下:
可以很清楚的看到,只有線程2是跑完了的,但是明明線程2已經(jīng)給flag賦值,線程1并沒(méi)有停止循環(huán),這就是flag這個(gè)變量沒(méi)有可見(jiàn)性,導(dǎo)致線程1一直不停止
解決的方法有兩種
第一:讓每個(gè)線程空余時(shí)間就去堆同步數(shù)據(jù)(顯然不合理)
第二:使用volatile關(guān)鍵字去修飾變量flag
讓我們加上volatile試試:
這回線程1總算是成功停止了,由此我們可得,volatile是可以讓變量具有可見(jiàn)性的。
學(xué)習(xí)編程不能只知道如何去使用,而是要知道原理,這樣才會(huì)有更多的薪資
那么volatile的底層是如何實(shí)現(xiàn)的呢?
如上面jvm運(yùn)行數(shù)據(jù)區(qū)的圖所示,所有的變量都是存在了堆當(dāng)中,而每個(gè)線程都是拿到他們的副本進(jìn)行計(jì)算和修改,volatile干了啥事呢,如下圖所示
這里我們介紹一個(gè)新的概念,叫總線(各位可以把它理解成進(jìn)行連接線程和堆內(nèi)存,在計(jì)算機(jī)的硬件當(dāng)中,也是有總線的,了解的朋友可以把它用相同概念理解一下)。
當(dāng)一個(gè)被volatile修飾的變量,在某一個(gè)線程當(dāng)中被修改時(shí),總線會(huì)監(jiān)聽(tīng)到這個(gè)變動(dòng),并且會(huì)讓其他線程中的這個(gè)變量失效,簡(jiǎn)而言之,當(dāng)線程2當(dāng)中堆flag進(jìn)行了修改,則會(huì)導(dǎo)致線程1當(dāng)中的flag失效,就是把這個(gè)線程1當(dāng)中的flag刪了。當(dāng)線程1中沒(méi)有flag了,它會(huì)重新去獲取flag,這個(gè)時(shí)候,就會(huì)使我們的變量flag具有了可見(jiàn)性。
現(xiàn)在我們已經(jīng)知道了,volatile的實(shí)行原理,那么它的底層是如何實(shí)現(xiàn)的?
眾所周知,java語(yǔ)言加載時(shí) -> class ->匯編語(yǔ)言 -> 機(jī)器語(yǔ)言,因?yàn)関olatile是個(gè)關(guān)鍵字,所以它的底層是一種匯編語(yǔ)法,被volatile修飾的變量其實(shí)就是給它加了個(gè)一個(gè)lock前綴指令。
也就是說(shuō),當(dāng)面試官問(wèn)到我們,如何手寫(xiě)一個(gè)volatile時(shí),我們可以說(shuō)在編譯的層面,添加一個(gè)lock前綴指令相當(dāng)于一個(gè)內(nèi)存屏障,它本身會(huì)提供三個(gè)功能
1)它會(huì)強(qiáng)制堆緩存的修改操作立即寫(xiě)入主存
2)如果是寫(xiě)操作,它會(huì)導(dǎo)致其他CPU中對(duì)應(yīng)的緩存行無(wú)效
3)它會(huì)確保指令重排序時(shí)不會(huì)吧其它的指令排到內(nèi)存屏障之前的位置,也不會(huì)之前的操作拍到內(nèi)存屏障之后
前面兩點(diǎn)很好理解,并且我們也進(jìn)行了進(jìn)一步的認(rèn)證,第三點(diǎn)可能有朋友不太明白,這就引出了我們下一個(gè)論點(diǎn),volatile可以保證一定的有序性
二、有序性
我們看下面三行代碼
int i=1; int j=2; i =i++;
在我們的理解當(dāng)中,程序時(shí)自上而下運(yùn)行的,先是第一行,再是第二行等,然而事實(shí)上,jvm可能會(huì)對(duì)代碼進(jìn)行重排序,比如它可能就會(huì)讓上面的這三行代碼變成下面的狀態(tài)
int i=1; i =i++; int j =2;
為什么會(huì)進(jìn)行重排序,目的是讓代碼執(zhí)行的速度更快,當(dāng)然它也不是隨便亂排的,排序的規(guī)則是根據(jù)代碼的依賴性進(jìn)行的判斷,簡(jiǎn)而言之就是在不影響結(jié)果的情況下進(jìn)行排序,感興趣的朋友可以自行去了解一下
這是java本身對(duì)程序保證的有序性,在不影響運(yùn)行結(jié)果的情況下進(jìn)行重排序,但是僅限于單線程的情況下,在多線程的情況中,并不能有效地保證程序的有序性
下圖為手寫(xiě)的一個(gè)單例模式,不做過(guò)多的贅述,左邊為代碼,右邊為翻譯的字節(jié)碼文件
通過(guò)上圖可以很清晰的看出,new OnlyObject這個(gè)操作重點(diǎn)分為了四步,
第一步:創(chuàng)建這個(gè)對(duì)象
第二步:調(diào)用這個(gè)類的構(gòu)造方法
第三步:添加指向(就是從私有線程當(dāng)中執(zhí)行堆)
第四步:加載
由于java對(duì)程序的重排序,會(huì)使第二步和第三步進(jìn)行調(diào)換位置,在單線程當(dāng)中不會(huì)有任何問(wèn)題,而在多線程當(dāng)中就有問(wèn)題了
看下圖代碼
當(dāng)線程1已經(jīng)完成添加指向時(shí),在堆當(dāng)中其實(shí)已經(jīng)分配了一個(gè)值,但是這時(shí)并沒(méi)有調(diào)用構(gòu)造方法,所以導(dǎo)致此時(shí)這個(gè)對(duì)象只是一個(gè)半成品對(duì)象 ,里面并不是我們想要的值。這時(shí)線程2走進(jìn)來(lái),他發(fā)現(xiàn)object并不為空,所以直接返回了,此時(shí)的程序跟我們的業(yè)務(wù)并不相符,所以我們需要使用volatile來(lái)保證我們的有序性。
總結(jié)
到此這篇關(guān)于Java并發(fā)編程之關(guān)鍵字volatile的文章就介紹到這了,更多相關(guān)Java并發(fā)編程關(guān)鍵字volatile內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java javax.annotation.Resource注解的詳解
這篇文章主要介紹了javax.annotation.Resource注解的詳解的相關(guān)資料,需要的朋友可以參考下2016-10-10restemplate請(qǐng)求亂碼之content-encoding=“gzip“示例詳解
RestTemplate從Spring3.0開(kāi)始支持的一個(gè)HTTP請(qǐng)求工具,它提供了常見(jiàn)的REST請(qǐng)求方案的模板,及一些通用的請(qǐng)求執(zhí)行方法 exchange 以及 execute,接下來(lái)通過(guò)本文給大家介紹restemplate請(qǐng)求亂碼之content-encoding=“gzip“,需要的朋友可以參考下2024-03-03Spring?Aop+Redis實(shí)現(xiàn)優(yōu)雅記錄接口調(diào)用情況
通常情況下,開(kāi)發(fā)完一個(gè)接口,無(wú)論是在測(cè)試階段還是生產(chǎn)上線,我們都需要對(duì)接口的執(zhí)行情況做一個(gè)監(jiān)控,所以本文為大家整理了Spring統(tǒng)計(jì)接口調(diào)用的多種方法,希望對(duì)大家有所幫助2023-06-06Web容器啟動(dòng)過(guò)程中如何執(zhí)行Java類
這篇文章主要介紹了Web容器啟動(dòng)過(guò)程中如何執(zhí)行Java類,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10SpringBoot在一定時(shí)間內(nèi)限制接口請(qǐng)求次數(shù)的實(shí)現(xiàn)示例
在項(xiàng)目中,接口的暴露在外面,很多人就會(huì)惡意多次快速請(qǐng)求,本文主要介紹了SpringBoot在一定時(shí)間內(nèi)限制接口請(qǐng)求次數(shù)的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2022-03-03Spring Boot讀取resources目錄文件方法詳解
這篇文章主要介紹了Spring Boot讀取resources目錄文件方法詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01Java網(wǎng)絡(luò)編程TCP實(shí)現(xiàn)文件上傳功能
這篇文章主要為大家詳細(xì)介紹了Java網(wǎng)絡(luò)編程TCP實(shí)現(xiàn)文件上傳功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07