欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java并發(fā)編程之關(guān)鍵字volatile的深入解析

 更新時間:2021年09月16日 14:23:09   作者:我叫葉湘?zhèn)? 
提高java的并發(fā)編程,就不得不提volatile關(guān)鍵字,不管是在面試還是實(shí)際開發(fā)中volatile都是一個應(yīng)該掌握的技能,這篇文章主要給大家介紹了關(guān)于Java并發(fā)編程之關(guān)鍵字volatile的相關(guān)資料,需要的朋友可以參考下

前言

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)文章

最新評論