讓你徹底理解volatile

原創(chuàng)文章&經(jīng)驗(yàn)總結(jié)&從校招到A廠一路陽(yáng)光一路滄桑

詳情請(qǐng)戳www.codercc.com

image

1. volatile簡(jiǎn)介

在上一篇文章中我們深入理解了java關(guān)鍵字synchronized,我們知道在java中還有一大神器就是關(guān)鍵volatile,可以說(shuō)是和synchronized各領(lǐng)風(fēng)騷,其中奧妙,我們來(lái)共同探討下。

通過(guò)上一篇的文章我們了解到synchronized是阻塞式同步,在線程競(jìng)爭(zhēng)激烈的情況下會(huì)升級(jí)為重量級(jí)鎖。而volatile就可以說(shuō)是java虛擬機(jī)提供的最輕量級(jí)的同步機(jī)制。但它同時(shí)不容易被正確理解,也至于在并發(fā)編程中很多程序員遇到線程安全的問(wèn)題就會(huì)使用synchronized。Java內(nèi)存模型告訴我們,各個(gè)線程會(huì)將共享變量從主內(nèi)存中拷貝到工作內(nèi)存,然后執(zhí)行引擎會(huì)基于工作內(nèi)存中的數(shù)據(jù)進(jìn)行操作處理。線程在工作內(nèi)存進(jìn)行操作后何時(shí)會(huì)寫到主內(nèi)存中?這個(gè)時(shí)機(jī)對(duì)普通變量是沒(méi)有規(guī)定的,而針對(duì)volatile修飾的變量給java虛擬機(jī)特殊的約定,線程對(duì)volatile變量的修改會(huì)立刻被其他線程所感知,即不會(huì)出現(xiàn)數(shù)據(jù)臟讀的現(xiàn)象,從而保證數(shù)據(jù)的“可見(jiàn)性”。

現(xiàn)在我們有了一個(gè)大概的印象就是:被volatile修飾的變量能夠保證每個(gè)線程能夠獲取該變量的最新值,從而避免出現(xiàn)數(shù)據(jù)臟讀的現(xiàn)象。

2. volatile實(shí)現(xiàn)原理

volatile是怎樣實(shí)現(xiàn)了?比如一個(gè)很簡(jiǎn)單的Java代碼:

instance = new Instancce() //instance是volatile變量

在生成匯編代碼時(shí)會(huì)在volatile修飾的共享變量進(jìn)行寫操作的時(shí)候會(huì)多出Lock前綴的指令(具體的大家可以使用一些工具去看一下,這里我就只把結(jié)果說(shuō)出來(lái))。我們想這個(gè)Lock指令肯定有神奇的地方,那么Lock前綴的指令在多核處理器下會(huì)發(fā)現(xiàn)什么事情了?主要有這兩個(gè)方面的影響:

  1. 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回系統(tǒng)內(nèi)存;
  2. 這個(gè)寫回內(nèi)存的操作會(huì)使得其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無(wú)效

為了提高處理速度,處理器不直接和內(nèi)存進(jìn)行通信,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2或其他)后再進(jìn)行操作,但操作完不知道何時(shí)會(huì)寫到內(nèi)存。如果對(duì)聲明了volatile的變量進(jìn)行寫操作,JVM就會(huì)向處理器發(fā)送一條Lock前綴的指令,將這個(gè)變量所在緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。但是,就算寫回到內(nèi)存,如果其他處理器緩存的值還是舊的,再執(zhí)行計(jì)算操作就會(huì)有問(wèn)題。所以,在多處理器下,為了保證各個(gè)處理器的緩存是一致的,就會(huì)實(shí)現(xiàn)緩存一致性協(xié)議,每個(gè)處理器通過(guò)嗅探在總線上傳播的數(shù)據(jù)來(lái)檢查自己緩存的值是不是過(guò)期了,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無(wú)效狀態(tài),當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里。因此,經(jīng)過(guò)分析我們可以得出如下結(jié)論:

  1. Lock前綴的指令會(huì)引起處理器緩存寫回內(nèi)存;
  2. 一個(gè)處理器的緩存回寫到內(nèi)存會(huì)導(dǎo)致其他處理器的緩存失效;
  3. 當(dāng)處理器發(fā)現(xiàn)本地緩存失效后,就會(huì)從內(nèi)存中重讀該變量數(shù)據(jù),即可以獲取當(dāng)前最新值。

這樣針對(duì)volatile變量通過(guò)這樣的機(jī)制就使得每個(gè)線程都能獲得該變量的最新值。

3. volatile的happens-before關(guān)系

經(jīng)過(guò)上面的分析,我們已經(jīng)知道了volatile變量可以通過(guò)緩存一致性協(xié)議保證每個(gè)線程都能獲得最新值,即滿足數(shù)據(jù)的“可見(jiàn)性”。我們繼續(xù)延續(xù)上一篇分析問(wèn)題的方式(我一直認(rèn)為思考問(wèn)題的方式是屬于自己,也才是最重要的,也在不斷培養(yǎng)這方面的能力),我一直將并發(fā)分析的切入點(diǎn)分為兩個(gè)核心,三大性質(zhì)。兩大核心:JMM內(nèi)存模型(主內(nèi)存和工作內(nèi)存)以及happens-before;三條性質(zhì):原子性,可見(jiàn)性,有序性(關(guān)于三大性質(zhì)的總結(jié)在以后得文章會(huì)和大家共同探討)。廢話不多說(shuō),先來(lái)看兩個(gè)核心之一:volatile的happens-before關(guān)系。

在六條happens-before規(guī)則中有一條是:volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫,happens-before于任意后續(xù)對(duì)這個(gè)volatile域的讀。下面我們結(jié)合具體的代碼,我們利用這條規(guī)則推導(dǎo)下:

public class VolatileExample {
    private int a = 0;
    private volatile boolean flag = false;
    public void writer(){
        a = 1;          //1
        flag = true;   //2
    }
    public void reader(){
        if(flag){      //3
            int i = a; //4
        }
    }
}

上面的實(shí)例代碼對(duì)應(yīng)的happens-before關(guān)系如下圖所示:

VolatileExample的happens-before關(guān)系推導(dǎo)

加鎖線程A先執(zhí)行writer方法,然后線程B執(zhí)行reader方法圖中每一個(gè)箭頭兩個(gè)節(jié)點(diǎn)就代碼一個(gè)happens-before關(guān)系,黑色的代表根據(jù)程序順序規(guī)則推導(dǎo)出來(lái),紅色的是根據(jù)volatile變量的寫happens-before 于任意后續(xù)對(duì)volatile變量的讀,而藍(lán)色的就是根據(jù)傳遞性規(guī)則推導(dǎo)出來(lái)的。這里的2 happen-before 3,同樣根據(jù)happens-before規(guī)則定義:如果A happens-before B,則A的執(zhí)行結(jié)果對(duì)B可見(jiàn),并且A的執(zhí)行順序先于B的執(zhí)行順序,我們可以知道操作2執(zhí)行結(jié)果對(duì)操作3來(lái)說(shuō)是可見(jiàn)的,也就是說(shuō)當(dāng)線程A將volatile變量 flag更改為true后線程B就能夠迅速感知。

4. volatile的內(nèi)存語(yǔ)義

還是按照兩個(gè)核心的分析方式,分析完happens-before關(guān)系后我們現(xiàn)在就來(lái)進(jìn)一步分析volatile的內(nèi)存語(yǔ)義(按照這種方式去學(xué)習(xí),會(huì)不會(huì)讓大家對(duì)知識(shí)能夠把握的更深,而不至于不知所措,如果大家認(rèn)同我的這種方式,不妨給個(gè)贊,小弟在此謝過(guò),對(duì)我是個(gè)鼓勵(lì))。還是以上面的代碼為例,假設(shè)線程A先執(zhí)行writer方法,線程B隨后執(zhí)行reader方法,初始時(shí)線程的本地內(nèi)存中flag和a都是初始狀態(tài),下圖是線程A執(zhí)行volatile寫后的狀態(tài)圖。

線程A執(zhí)行volatile寫后的內(nèi)存狀態(tài)圖

當(dāng)volatile變量寫后,線程中本地內(nèi)存中共享變量就會(huì)置為失效的狀態(tài),因此線程B再需要讀取從主內(nèi)存中去讀取該變量的最新值。下圖就展示了線程B讀取同一個(gè)volatile變量的內(nèi)存變化示意圖。

線程B讀volatile后的內(nèi)存狀態(tài)圖

從橫向來(lái)看,線程A和線程B之間進(jìn)行了一次通信,線程A在寫volatile變量時(shí),實(shí)際上就像是給B發(fā)送了一個(gè)消息告訴線程B你現(xiàn)在的值都是舊的了,然后線程B讀這個(gè)volatile變量時(shí)就像是接收了線程A剛剛發(fā)送的消息。既然是舊的了,那線程B該怎么辦了?自然而然就只能去主內(nèi)存去取啦。

好的,我們現(xiàn)在兩個(gè)核心:happens-before以及內(nèi)存語(yǔ)義現(xiàn)在已經(jīng)都了解清楚了。是不是還不過(guò)癮,突然發(fā)現(xiàn)原來(lái)自己會(huì)這么愛(ài)學(xué)習(xí)(微笑臉),那我們下面就再來(lái)一點(diǎn)干貨----volatile內(nèi)存語(yǔ)義的實(shí)現(xiàn)。

4.1 volatile的內(nèi)存語(yǔ)義實(shí)現(xiàn)

我們都知道,為了性能優(yōu)化,JMM在不改變正確語(yǔ)義的前提下,會(huì)允許編譯器和處理器對(duì)指令序列進(jìn)行重排序,那如果想阻止重排序要怎么辦了?答案是可以添加內(nèi)存屏障。

內(nèi)存屏障

JMM內(nèi)存屏障分為四類見(jiàn)下圖,

內(nèi)存屏障分類表

java編譯器會(huì)在生成指令系列時(shí)在適當(dāng)?shù)奈恢脮?huì)插入內(nèi)存屏障指令來(lái)禁止特定類型的處理器重排序。為了實(shí)現(xiàn)volatile的內(nèi)存語(yǔ)義,JMM會(huì)限制特定類型的編譯器和處理器重排序,JMM會(huì)針對(duì)編譯器制定volatile重排序規(guī)則表:

volatile重排序規(guī)則表

"NO"表示禁止重排序。為了實(shí)現(xiàn)volatile內(nèi)存語(yǔ)義時(shí),編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來(lái)禁止特定類型的處理器重排序。對(duì)于編譯器來(lái)說(shuō),發(fā)現(xiàn)一個(gè)最優(yōu)布置來(lái)最小化插入屏障的總數(shù)幾乎是不可能的,為此,JMM采取了保守策略:

  1. 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障;
  2. 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障;
  3. 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障;
  4. 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障。

需要注意的是:volatile寫是在前面和后面分別插入內(nèi)存屏障,而volatile讀操作是在后面插入兩個(gè)內(nèi)存屏障

StoreStore屏障:禁止上面的普通寫和下面的volatile寫重排序;

StoreLoad屏障:防止上面的volatile寫與下面可能有的volatile讀/寫重排序

LoadLoad屏障:禁止下面所有的普通讀操作和上面的volatile讀重排序

LoadStore屏障:禁止下面所有的普通寫操作和上面的volatile讀重排序

下面以兩個(gè)示意圖進(jìn)行理解,圖片摘自相當(dāng)好的一本書(shū)《java并發(fā)編程的藝術(shù)》。

volatile寫插入內(nèi)存屏障示意圖
volatile讀插入內(nèi)存屏障示意圖

5. 一個(gè)示例

我們現(xiàn)在已經(jīng)理解volatile的精華了,文章開(kāi)頭的那個(gè)問(wèn)題我想現(xiàn)在我們都能給出答案了。更正后的代碼為:

public class VolatileDemo {
    private static volatile boolean isOver = false;

    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!isOver) ;
            }
        });
        thread.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        isOver = true;
    }
}

注意不同點(diǎn),現(xiàn)在已經(jīng)將isOver設(shè)置成了volatile變量,這樣在main線程中將isOver改為了true后,thread的工作內(nèi)存該變量值就會(huì)失效,從而需要再次從主內(nèi)存中讀取該值,現(xiàn)在能夠讀出isOver最新值為true從而能夠結(jié)束在thread里的死循環(huán),從而能夠順利停止掉thread線程。現(xiàn)在問(wèn)題也解決了,知識(shí)也學(xué)到了:)。(如果覺(jué)得還不錯(cuò),請(qǐng)點(diǎn)贊,是對(duì)我的一個(gè)鼓勵(lì)。)

參考文獻(xiàn)

《java并發(fā)編程的藝術(shù)》

最后編輯于
?著作權(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)容