java并發(fā)之volatile

在Java并發(fā)編程中,volatile和synchronized都扮演者重要的角色。volatile又被成為輕量級(jí)的synchronized,它保證了共享變量的可見(jiàn)性。

注:何謂可見(jiàn)性?
通俗點(diǎn)兒說(shuō),可見(jiàn)性就是當(dāng)一個(gè)線程修改一個(gè)共享變量時(shí),其他的線程也可以讀到修改后的值。

如果volatile使用得當(dāng),由于不會(huì)引起線程的上下文切換和調(diào)度,它的開(kāi)銷會(huì)比synchronized關(guān)鍵字小很多。本文將會(huì)深入分析volatile關(guān)鍵字實(shí)現(xiàn)原理,相信通過(guò)本文,大家可以更好的使用volatile關(guān)鍵字。

如何保證可見(jiàn)性

在Java內(nèi)存模型(Java Memory Model,JMM)中,線程和主存之間存在這樣的抽象關(guān)系:

  1. 線程之間的共享變量存放在主內(nèi)存(Main Memory)中;

  2. 每個(gè)線程都有一個(gè)私有的本地內(nèi)存(Local Memory),存放了當(dāng)前線程共享變量的副本。

那針對(duì)被volatile修飾的變量的讀/寫(xiě)到底是什么樣子的呢?

  1. 當(dāng)寫(xiě)一個(gè)volatile變量時(shí),JMM會(huì)把當(dāng)前線程的本地內(nèi)存中的共享變量值刷新到主內(nèi)存中;

  2. 當(dāng)讀一個(gè)volatile變量時(shí),JMM會(huì)把當(dāng)前線程的本地內(nèi)存置為無(wú)效,然后從主內(nèi)存中讀取共享變量。

對(duì)Java內(nèi)存模型有一些了解的一定對(duì)指令重排序不陌生,一個(gè)程序在運(yùn)行中,編譯器和處理器為了優(yōu)化程序性能,會(huì)對(duì)指令序列進(jìn)行重排序,那這時(shí)候就有問(wèn)題了,那既然指令會(huì)被重新排序,volatile是怎么保證內(nèi)存可見(jiàn)性呢?萬(wàn)一被重排了,內(nèi)存可見(jiàn)不就會(huì)有問(wèn)題了~當(dāng)然咯,為了保證volatile的內(nèi)存可見(jiàn)性,編譯器在生成字節(jié)碼的時(shí)候會(huì)在指令序列中插入內(nèi)存屏障(Memory Barriers)來(lái)禁止特定類型的編譯器和處理器重排序。一旦內(nèi)存屏障被插入,就相當(dāng)于告訴了編譯器和處理器:不管是什么指令都不能和內(nèi)存屏障進(jìn)行重排序。

注:內(nèi)存屏障(Memory Barriers):一組CPU指令,用于實(shí)現(xiàn)對(duì)內(nèi)存操作的順序限制。

我們還是以一個(gè)簡(jiǎn)單的例子來(lái)解釋下:


volatile測(cè)試demo

從test()方法可以看出:

  • 如果d不是volatile變量,1/2/3三個(gè)語(yǔ)句是可以進(jìn)行隨意進(jìn)行指令重排序的;

注:為什么4不能和1/2/3一起指令重排序?因?yàn)?和1/2/3存在數(shù)據(jù)依賴關(guān)系,編譯器和處理器在重排序時(shí)會(huì)遵守?cái)?shù)據(jù)依賴性,不會(huì)改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序。所以4不會(huì)參與到1/2/3的重排序行列。

  • d被volatile修飾后,會(huì)在1/2后插入一個(gè)內(nèi)存屏障,此時(shí),1/2可以隨意進(jìn)行指令重排序,3不再參與到重排序行列。

接下來(lái)我們?cè)趚86下通過(guò)工具生成的匯編指令來(lái)看看對(duì)volatile變量進(jìn)行讀寫(xiě)操作時(shí)CPU會(huì)干什么。

instance = new Singleton(); //instance是volatile變量

匯編指令:
lock addl $0x0, (%rsp)

從匯編指令可以看出,volatile變量在寫(xiě)操作之前使用了lock前綴,lock前綴指令在多處理器下會(huì)引發(fā)兩件事情:

  1. 將當(dāng)前處理器的緩存行里的數(shù)據(jù)寫(xiě)回到內(nèi)存。lock前綴指令的在執(zhí)行期間會(huì)產(chǎn)生LOCK#信號(hào),該信號(hào)會(huì)鎖住總線,導(dǎo)致其他CPU不能訪問(wèn)總線。由于鎖總線開(kāi)銷比較大,在最近的處理器中,LOCK#信號(hào)不鎖總線了,而是鎖緩存并回寫(xiě)到主存,用緩存一直性機(jī)制來(lái)確保修改的原子性(該操作又被稱之為緩存鎖定);

  2. 寫(xiě)回內(nèi)存的操作會(huì)使其他CPU的緩存無(wú)效。IA-32處理器和Intel 64處理器使用MESI(修改,獨(dú)占,共享,無(wú)效)控制協(xié)議去維護(hù)內(nèi)部緩存和其他處理器緩存的一致性。IA-32和Intel 64處理器使用嗅探技術(shù)保證各處理器緩存和系統(tǒng)內(nèi)存的數(shù)據(jù)在總線上保持一致。如果一個(gè)處理器通過(guò)嗅探檢測(cè)到其他處理器準(zhǔn)備寫(xiě)內(nèi)存,并且該內(nèi)存地址處于共享狀態(tài),該處理器會(huì)將它的緩存行置為無(wú)效,下次再操作該內(nèi)存地址時(shí)會(huì)重新把數(shù)據(jù)讀到緩存行中。

使用場(chǎng)景

多嘴兩句,volatile雖然在某些情況下性能要優(yōu)于synchronized,但是由于volatile無(wú)法保證操作的原子性,所以volatile是無(wú)法替代synchronized的。

通常來(lái)說(shuō),使用volatile必須具備以下2個(gè)條件:

  • 對(duì)變量的寫(xiě)操作不依賴于當(dāng)前值,比如++操作,volatile不能保證多線程情況下操作的正確性的;

  • 該變量沒(méi)有包含在具有其他變量的不變式中。

下面列舉兩個(gè)我們平常在工作中使用較多的場(chǎng)景:

  1. 狀態(tài)標(biāo)記
    需求描述:根據(jù)狀態(tài)標(biāo)記進(jìn)行消息發(fā)送;
    代碼示例:
public class MessageProcessor {

    private volatile boolean flag;

    public void process() {
        if (flag) {
            sendMessage();
        } else {
            recordLog();
        }
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

用volatile修飾flag,如果有其他線程修改過(guò)該標(biāo)識(shí),其他線程都可以拿到最新值,根據(jù)最新值各個(gè)線程可以進(jìn)行相關(guān)的操作。

  1. 單例模式的double check

    單例模式的double check

    很多人在使用double check的時(shí)候都不用volatile修飾,程序也能正常運(yùn)行,但是其實(shí)不使用volatile修飾是有問(wèn)題的。那會(huì)有什么問(wèn)題呢?
    其實(shí)會(huì)出問(wèn)題的主要是instance = new Singleton();,它并不是一個(gè)原子操作,在JVM中,這行代碼主要完成了這樣3件事:

    a. 為對(duì)象分配內(nèi)存空間;

    b. 初始化對(duì)象;

    c. 將instance對(duì)象指向分配的內(nèi)存空間,執(zhí)行完這一步驟,instance就不為null了。

    在指令重排序時(shí),a-b-c的順序可能會(huì)被重排成a-c-b,如果現(xiàn)在執(zhí)行順序被重排成a-c-b,在單線程情況下不會(huì)影響程序執(zhí)行的結(jié)果,但是在多線程情況下就不一樣了,如果線程A執(zhí)行了指令c,此時(shí)instance實(shí)例還沒(méi)有被初始化好,但是已經(jīng)不為null了,剛好線程B執(zhí)行到if (null == instance),發(fā)現(xiàn)instance不為空,隨即返回,但是得到的卻是未被完全初始化的實(shí)例,在使用的時(shí)候就會(huì)有問(wèn)題。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容