溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

volatile的實(shí)現(xiàn)原理是什么

發(fā)布時(shí)間:2021-06-18 15:55:46 來(lái)源:億速云 閱讀:98 作者:Leah 欄目:大數(shù)據(jù)

這篇文章將為大家詳細(xì)講解有關(guān)volatile的實(shí)現(xiàn)原理是什么,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對(duì)相關(guān)知識(shí)有一定的了解。

Java編程語(yǔ)言允許線(xiàn)程訪問(wèn)共享變量,為了確保共享變量能夠被準(zhǔn)確和一致性的更新,線(xiàn)程應(yīng)該確保通過(guò)排他鎖單獨(dú)獲取這個(gè)變量。

這句話(huà)可能說(shuō)的比較繞,我們先來(lái)看一段代碼:

public class VolatileTest implements Runnable {
    private boolean flag = false;
    @Override
    public void run() {
        while (!flag){
            
        }
        System.out.println("線(xiàn)程結(jié)束運(yùn)行...");
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
    public static void main(String[] args) throws InterruptedException {
        VolatileTest v = new VolatileTest();
        Thread t1 = new Thread(v);
        t1.start();
        Thread.sleep(2000);
        v.setFlag(true);
    }
}

這段代碼的運(yùn)行結(jié)果:

volatile的實(shí)現(xiàn)原理是什么

可以看到盡管在代碼中調(diào)用了v.setFlag(false)方法,線(xiàn)程也沒(méi)有結(jié)束運(yùn)行。這是因?yàn)樵谏厦娴拇a中,實(shí)際上是有2個(gè)線(xiàn)程在運(yùn)行,一個(gè)是main線(xiàn)程,一個(gè)是在main線(xiàn)程中創(chuàng)建的t1線(xiàn)程。因此我們可以看到在線(xiàn)程中的變量是互不可見(jiàn)的。 要理解線(xiàn)程中變量的可見(jiàn)性,我們需要先理解Java的內(nèi)存模型。

<font color="#EE30A7">Java內(nèi)存模型</font>

在Java中,所有的實(shí)例域、靜態(tài)變量和數(shù)組元素都存儲(chǔ)在堆內(nèi)存中,堆內(nèi)存在線(xiàn)程之間是共享的。局部變量,方法定義參數(shù)和異常數(shù)量參數(shù)是存放在Java虛擬機(jī)棧上面的。Java虛擬機(jī)棧是線(xiàn)程私有的因此不會(huì)在線(xiàn)程之間共享,它們不存在內(nèi)存可見(jiàn)性的問(wèn)題,也不受內(nèi)存模型的影響。

Java內(nèi)存模型(Java Memory Model 簡(jiǎn)稱(chēng) JMM),決定一個(gè)一個(gè)線(xiàn)程對(duì)共享變量的寫(xiě)入何時(shí)對(duì)其它線(xiàn)程可見(jiàn)。JMM定義了線(xiàn)程和主內(nèi)存之間的抽象關(guān)系:

線(xiàn)程之間共享變量存儲(chǔ)在主內(nèi)存中,每個(gè)線(xiàn)程都有一個(gè)私有的本地內(nèi)存,本地內(nèi)存中存儲(chǔ)了該線(xiàn)程共享變量的副本。本地內(nèi)存是JMM的一個(gè)抽象概率,并不真實(shí)的存在。它涵蓋了緩存,寫(xiě)緩存區(qū),寄存器以及其他的硬件和編譯優(yōu)化。

Java內(nèi)存模型的抽象概念圖如下所示:

volatile的實(shí)現(xiàn)原理是什么

看完了Java內(nèi)存模型的概念,我們?cè)賮?lái)看看內(nèi)存模型中主內(nèi)存是如何和線(xiàn)程本地內(nèi)存之間交互的。

<font color="#EE30A7">主內(nèi)存和本地內(nèi)存間的交互</font>

主內(nèi)存和本地內(nèi)存的交互即一個(gè)變量是如何從主內(nèi)存中拷貝到本地內(nèi)存又是如何從本地內(nèi)存中回寫(xiě)到主內(nèi)存中的實(shí)現(xiàn),Java內(nèi)存模型提供了8中操作來(lái)完成主內(nèi)存和本地內(nèi)存之間的交互。它們分別如下:

  • <span >lock(鎖定)</span>:作用于主內(nèi)存的變量,它把一個(gè)變量標(biāo)識(shí)為一條線(xiàn)程獨(dú)占的狀態(tài)。

  • <span >unlock(解鎖)</span>:作用于主內(nèi)存的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放后的變量才能被其它線(xiàn)程鎖定。

  • <span >read(讀?。?lt;/span>:作用于主內(nèi)存的變量,它把一個(gè)變量從主內(nèi)存?zhèn)鬏數(shù)骄€(xiàn)程的本地內(nèi)存中,以便隨后的load動(dòng)作使用。

  • <span >load(載入)</span>:作用于本地內(nèi)存的變量,它把read操作從主內(nèi)存中的到的變量值放入本地內(nèi)存的變量副本中。

  • <span >use(使用)</span>:作用于本地內(nèi)存的變量,它把本地內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用到變量值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。

  • <span >assign(賦值)</span>:作用于本地內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的變量賦予給本地內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。

  • <span >store(存儲(chǔ))</span>:作用于本地內(nèi)存的變量,它把本地內(nèi)存中的變量的值傳遞給主內(nèi)存中,以便后面的write操作使用。

  • <span >write(寫(xiě)入)</span>:作用于主內(nèi)存的變量,它把store操作從本地內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。

從上面8種操作中,我們可以看出,當(dāng)一個(gè)變量從主內(nèi)存復(fù)制到線(xiàn)程的本地內(nèi)存中時(shí),需要順序的執(zhí)行read和load操作,當(dāng)一個(gè)變量從本地內(nèi)存同步到主內(nèi)存中時(shí),需要順序的執(zhí)行store和write操作。Java內(nèi)存模型只要求上述的2組操作是順序的執(zhí)行的,但并不要求連續(xù)執(zhí)行。比如對(duì)主內(nèi)存中的變量a 和 b 進(jìn)行訪問(wèn)時(shí),有可能出現(xiàn)的順序是read a read b load b load a。除此之外,Java內(nèi)存模型還規(guī)定了在執(zhí)行上述8種基本操作時(shí)必須滿(mǎn)足以下規(guī)則:

  • 不允許read和load,store和write操作單獨(dú)出現(xiàn),這2組操作必須是成對(duì)的。

  • 不允許一個(gè)線(xiàn)程丟棄它最近的assign操作。即變量在線(xiàn)程的本地內(nèi)存中改變后必須同步到主內(nèi)存中。

  • 不允許一個(gè)線(xiàn)程無(wú)原因的把數(shù)據(jù)從線(xiàn)程的本地內(nèi)存同步到主內(nèi)存中。

  • 不允許線(xiàn)程的本地內(nèi)存中使用一個(gè)未被初始化的變量。

  • 一個(gè)變量在同一時(shí)刻只允許一個(gè)線(xiàn)程對(duì)其進(jìn)行l(wèi)ock操作,但是一個(gè)線(xiàn)程可以對(duì)一個(gè)變量進(jìn)行多次的lock操作,當(dāng)線(xiàn)程對(duì)同一變量進(jìn)行了多次lock操作后需要進(jìn)行同樣次數(shù)的unlock操作才能將變量釋放。

  • 如果一個(gè)變量執(zhí)行了lock操作,則會(huì)清空本地內(nèi)存中變量的拷貝,當(dāng)需要使用這個(gè)變量時(shí)需要重新執(zhí)行read和load操作。

  • 如果一個(gè)變量沒(méi)有執(zhí)行l(wèi)ock操作,那么就不能對(duì)這個(gè)變量執(zhí)行unlock操作,同樣也不允許unlock一個(gè)被其它線(xiàn)程執(zhí)行了lock操作的變量。也就是說(shuō)lock 和unlock操作是成對(duì)出現(xiàn)的并且是在同一個(gè)線(xiàn)程中。

  • 對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須將這個(gè)變量的值同步到主內(nèi)存中去。

<font color="#EE30A7">volatile 內(nèi)存語(yǔ)義之可見(jiàn)性</font>

大概了解了Java的內(nèi)存模型后,我們?cè)倏瓷厦娴拇a結(jié)果我們將很好理解為什么是這樣子的了。首先主內(nèi)存中flag的值是false,在t1線(xiàn)程執(zhí)行時(shí),依次執(zhí)行的操作有read、load和use操作,這個(gè)時(shí)候t1線(xiàn)程的本地內(nèi)存中flag的值也是false,線(xiàn)程會(huì)一直執(zhí)行。當(dāng)main線(xiàn)程調(diào)用v.setFlag(true)方法時(shí),main線(xiàn)程中的falg被賦值成了true,因?yàn)槭褂昧薬ssign操作,因此main線(xiàn)程中本地內(nèi)存的flag值將同步到主內(nèi)存中去,這時(shí)主內(nèi)存中的flag的值為true。但是t1線(xiàn)程沒(méi)有再次執(zhí)行read 和 load操作,因此t1線(xiàn)程中flag的值任然是false,所以t1線(xiàn)程不會(huì)終止運(yùn)行。想要正確的停止t1線(xiàn)程,只需要在flag變量前加上volatile修飾符即可,因?yàn)関olatile保證了變量的可見(jiàn)性。既然volatile在各個(gè)線(xiàn)程中是一致的,那么volatile是否能夠保證在并發(fā)情況下的安全呢?答案是否定的,因?yàn)関olatile不能保證變量的原子性。示例如下:

public class VolatileTest2 implements Runnable {
    private volatile int i = 0;
    @Override
    public void run() {
        for (int j=0;j<1000;j++) {
            i++;
        }
    }
    public int getI() {
        return i;
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileTest2 v2 = new VolatileTest2();
        for (int i=0;i<100;i++){
            new Thread(v2).start();
        }
        Thread.sleep(5000);
        System.out.println(v2.getI());
    }
}

這段代碼啟動(dòng)了100線(xiàn)程,每個(gè)線(xiàn)程都對(duì)i變量進(jìn)行1000次的自增操作,若果這段代碼能夠正確的運(yùn)行,那么正確的結(jié)果應(yīng)該是100000,但是實(shí)際并非如此,實(shí)際運(yùn)行的結(jié)果是少于100000的,這是因?yàn)関olatile不能保證i++這個(gè)操作的原子性。我們用javap反編譯這段代碼,截取run()方法的代碼片段如下:

 public void run();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: sipush        1000
         6: if_icmpge     25
         9: aload_0
        10: dup
        11: getfield      #2                  // Field i:I
        14: iconst_1
        15: iadd
        16: putfield      #2                  // Field i:I
        19: iinc          1, 1
        22: goto          2
        25: return

我們發(fā)現(xiàn)i++雖然只有一行代碼,但是在Class文件中卻是由4條字節(jié)碼指令組成的。從上面字節(jié)碼片段,我們很容易分析出并發(fā)失敗的原因:當(dāng)getfield指令把變量i的值取到操作棧時(shí),volatile關(guān)鍵字保證了i的值在此時(shí)的正確性,但是在執(zhí)行iconst_1和iadd指令時(shí),i的值可能已經(jīng)被其它的線(xiàn)程改變,此時(shí)再執(zhí)行putfield指令時(shí),就會(huì)把一個(gè)過(guò)期的值回寫(xiě)到主內(nèi)存中去了。由于volatile只保證了變量的可見(jiàn)性,在不符合以下規(guī)則的場(chǎng)景中,我們?nèi)稳恍枰褂面i來(lái)保證并發(fā)的正確性。

  • 運(yùn)算結(jié)果結(jié)果并不依賴(lài)變量的當(dāng)前值,或者能夠確保只有單一的線(xiàn)程修改了變量的值

  • 變量不需要與其他的狀態(tài)變量共同參與不變約束

<font color="#EE30A7">volatile 內(nèi)存語(yǔ)義之禁止重排序</font>

在介紹volatile的禁止重排序之前,我們先來(lái)了解下什么是重排序。重排序是指編譯器和處理器為了優(yōu)化程序性能而對(duì)指令進(jìn)行重新排序的一種手段。那么重排序有哪些規(guī)則呢?不可能任何代碼都可以重排序,如果是這樣的話(huà),那么在單線(xiàn)程中,我們將不能得到明確的知道運(yùn)行的結(jié)果。重排序規(guī)則如下:

  • 具有數(shù)據(jù)依賴(lài)性操作不能重排序,數(shù)據(jù)依賴(lài)性是指兩個(gè)操作訪問(wèn)同一個(gè)變量,如果一個(gè)操作是寫(xiě)操作,那么這兩個(gè)操作就存在數(shù)據(jù)依賴(lài)性。

  • as-if-serial語(yǔ)義,as-if-serial語(yǔ)義的意思是,不管怎么重排序,單線(xiàn)程的程序執(zhí)行結(jié)果是不會(huì)改變的。

既然volatile禁止重排序,那是不是重排序?qū)Χ嗑€(xiàn)程有影響呢?我們先來(lái)看下面的代碼示例

public class VolatileTest3 {
    int a = 0;
    boolean flag = false;

    public void write(){
        a = 1;                 // 1
        flag = true;           // 2
    }

    public void read(){
        if(flag){               // 3
            int i = a*a;        // 4
            System.out.println("i的值為:"+i);
        }

    }
}

此時(shí)有2個(gè)線(xiàn)程A和B,線(xiàn)程A先執(zhí)行write()方法,雖有B執(zhí)行read()方法,在B線(xiàn)程執(zhí)行到第4步時(shí),i的結(jié)果能正確得到嗎?結(jié)論是 不一定 ,因?yàn)椴襟E1和2沒(méi)有數(shù)據(jù)依賴(lài)關(guān)系,因此編譯器和處理器可能對(duì)這2個(gè)操作進(jìn)行重排序。同樣步驟3和4也沒(méi)有數(shù)據(jù)依賴(lài)關(guān)系,編譯器和處理器也可以對(duì)這個(gè)2個(gè)操作進(jìn)行重排序,我們來(lái)看看這兩中重排序帶來(lái)的效果:

volatile的實(shí)現(xiàn)原理是什么

重上面圖片,這2組重排序都會(huì)破壞多線(xiàn)程的運(yùn)行結(jié)果。了解了重排序的概率和知道了重排序?qū)Χ嗑€(xiàn)程的影響,我們知道了volatile為什么需要禁止重排序,那JMM到底是如何實(shí)現(xiàn)volatile禁止重排序的呢?下面我們就來(lái)探討下JMM是如何實(shí)現(xiàn)volatile禁止重排序的。

前面提到過(guò),重排序分為編譯器重排序和處理器重排序,為了實(shí)現(xiàn)volatile內(nèi)存語(yǔ)義,JMM分別對(duì)這兩種重排序進(jìn)行了現(xiàn)在。下圖是JMM對(duì)編譯器重排序指定的volatile規(guī)則:

volatile的實(shí)現(xiàn)原理是什么

從上面圖中我們可以分析出:

  • 當(dāng)?shù)谝粋€(gè)操作為volatile讀時(shí),無(wú)能第二個(gè)操作是什么,都不允許重排序。這個(gè)規(guī)則確保了volatile讀之后的操作不能重排序到volatile讀之前。

  • 當(dāng)?shù)诙€(gè)操作為volatile寫(xiě)時(shí),無(wú)論第一個(gè)操作是什么,都不允許重排序。這個(gè)規(guī)則確保了volatile寫(xiě)之前的操作不能重排序到volatile寫(xiě)之后。

  • 當(dāng)?shù)谝粋€(gè)操作是volatile寫(xiě),第二個(gè)操作是volatile讀時(shí),不允許重排序。

為了實(shí)現(xiàn)volatile內(nèi)存語(yǔ)義,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來(lái)禁止特定類(lèi)型處理器的重排序,在JMM中,內(nèi)存屏障的插入策略如下:

  • <font color="red">在每個(gè)volatile寫(xiě)操作之前插入一個(gè)StoreStore屏障</font>

  • <font color="red">在每個(gè)volatile寫(xiě)操作之后插入一個(gè)StoreLoad屏障</font>

  • <font color="red">在每個(gè)volatile讀操作之后插入一個(gè)LoadLoad屏障</font>

  • <font color="red">在每個(gè)volatile讀操作之后插入一個(gè)LoadStore屏障</font>

StoreStore屏障可以保證在volatile寫(xiě)之前,前面所有的普通讀寫(xiě)操作同步到主內(nèi)存中

StoreLoad屏障可以保證防止前面的volatile寫(xiě)和后面有可能出現(xiàn)的volatile度/寫(xiě)進(jìn)行重排序

LoadLoad屏障可以保證防止下面的普通讀操作和上面的volatile讀進(jìn)行重排序

LoadStore屏障可以保存防止下面的普通寫(xiě)操作和上面的volatile讀進(jìn)行重排序

上面的內(nèi)存屏障策略可以保證任何程序都能得到正確的volatile內(nèi)存語(yǔ)義。我們以下面代碼來(lái)分析

public class VolatileTest3 {
    int a = 0;
    volatile boolean flag = false;

    public void write(){
        a = 1;                 // 1
        flag = true;           // 2
    }

    public void read(){
        if(flag){               // 3
            int i = a*a;        // 4
        }
    }
}

volatile的實(shí)現(xiàn)原理是什么

通過(guò)上面的示例我們分析了volatile指令的內(nèi)存屏蔽策略,但是這種內(nèi)存屏障的插入策略是非常保守的,在實(shí)際執(zhí)行時(shí),只要不改變volatile寫(xiě)/讀的內(nèi)存語(yǔ)義,編譯器可以根據(jù)具體情況來(lái)省略不必要的屏障。如下示例:

class VolatileBarrierExample {
	int a;
	volatile int v1 = 1;
	volatile int v2 = 2;
	void readAndWrite() {
		int i = v1; // 第一個(gè)volatile讀
		int j = v2; // 第二個(gè)volatile讀
		a = i + j; // 普通寫(xiě)
		v1 = i + 1; // 第一個(gè)volatile寫(xiě)
		v2 = j * 2; // 第二個(gè) volatile寫(xiě)
	} 
}

上述代碼,編譯器在生成字節(jié)碼時(shí),可能做了如下優(yōu)化

volatile的實(shí)現(xiàn)原理是什么 

關(guān)于volatile的實(shí)現(xiàn)原理是什么就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI