<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          原創(chuàng)|《菜鳥讀并發(fā)》java內(nèi)存模型之volatile深入解讀

          共 6495字,需瀏覽 13分鐘

           ·

          2020-07-11 11:31

          點(diǎn)擊上方“碼農(nóng)進(jìn)階之路”,選擇設(shè)為星標(biāo)

          回復(fù)面經(jīng)獲取面試資料

          在閱讀本文前,請(qǐng)思考以下的面試題?

          • volatile是什么?
          • volatile的特性
          • volatile是如何保證可見性的?
          • volatile是如何保證有序性的?
          • volatile可以保證原子性嗎?
          • 使用volatile變量的條件是什么?
          • volatile和synchronized的區(qū)別
          • volatile和atomic原子類的區(qū)別是什么?

          這一章主要是講解volatile的原理,在開始本文前,我們來看一張volatile的思維導(dǎo)圖,先有個(gè)直觀的認(rèn)識(shí)。e93a71fd7d854f291466d1808da6c3e7.webp

          什么是volatile

          目前的操作系統(tǒng)大多數(shù)都是多CPU,當(dāng)多線程對(duì)一個(gè)共享變量進(jìn)行操作時(shí),會(huì)出現(xiàn)數(shù)據(jù)一致性問題

          Java編程語言允許線程訪問共享變量,那么為了確保共享變量能被準(zhǔn)確和一致的更新,線程應(yīng)該確保通過排他鎖單獨(dú)獲得這個(gè)變量,或者把這個(gè)變量聲明成volatile,可以理解volatile是輕量級(jí)的synchronized

          使用volatile可以在Java線程內(nèi)存模型確保所有線程看到這個(gè)變量的值是一致的,在多個(gè)處理器中保證了共享變量的“可見性”

          volatile兩核心三性質(zhì)

          兩大核心:JMM內(nèi)存模型(主內(nèi)存和工作內(nèi)存)以及happens-before

          三條性質(zhì):原子性,可見性,有序性

          volatile性質(zhì)

          1. 保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見性,即一個(gè)線程修改了某個(gè)變量的值,這新值對(duì)其他線程來說是立即可見的。(實(shí)現(xiàn)可見性
          2. 禁止進(jìn)行指令重排序。(實(shí)現(xiàn)有序性
          3. 只能保證對(duì)單次讀/寫的原子性。i++ 這種操作不能保證原子性。(不能實(shí)現(xiàn)原子性
          4. volatile不會(huì)引起上下文的切換和調(diào)度

          總結(jié):volatile保證了可見性和有序性,同時(shí)可以保證單次讀/寫的原子性

          相關(guān)的Cpu術(shù)語說明

          b34865b589dfe969090c304d2990dca9.webp

          什么是可見性?

          在單核cpu的石器時(shí)代,我們所有的線程都是在一顆CPU上執(zhí)行,CPU緩存與內(nèi)存的數(shù)據(jù)一致性容易解決。因?yàn)樗芯€程都是操作同一個(gè)CPU的緩存,一個(gè)線程對(duì)緩存的寫,對(duì)另外一個(gè)線程來說一定是可見的。

          例如在下面的圖中,線程A和線程B都是操作同一個(gè)CPU里面的緩存,所以線程A更新了變量a的值,那么線程B之后再訪問變量 a,得到的一定是 a 的最新值(線程 A 寫過的值)。

          11f8592f632ea653a12b402747919cd0.webpfile

          在多核CPU的時(shí)代,每顆 CPU 都有自己的緩存,這時(shí) CPU 緩存與內(nèi)存的數(shù)據(jù)一致性就沒那么容易解決了,當(dāng)多個(gè)線程在不同的CPU上執(zhí)行時(shí),這些線程操作的是不同的CPU緩存。比如下圖中,線程A操作的是CPU-1上的緩存,而線程B操作的是CPU-2上的緩存,很明顯,這個(gè)時(shí)候線程A對(duì)變量a的操作對(duì)于線程B而言就不具備可見性了。這個(gè)就屬于硬件程序員給軟件程序員挖的“坑”。

          為了提高處理速度,處理器不直接和內(nèi)存進(jìn)行通信,而是先將系統(tǒng)內(nèi)存的值讀到內(nèi)部緩存(L1,L2或者其他)后再進(jìn)行操作,但是操作完不知道何時(shí)再寫回內(nèi)存。

          786a7ebae39f61a125282f0ac239e596.webpfile

          從上面的分析,我們可以知道,多核的CPU緩存會(huì)導(dǎo)致的可見性問題。

          volatile是如何保證可見性的

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

          讓我們來看看在處理器下通過工具獲取JIT編譯器生成的匯編指令來查看對(duì)volatile進(jìn)行寫操作的時(shí)候,cpu會(huì)做什么事?

          轉(zhuǎn)換成匯編代碼如下:

          3bb7bc61c5b5fdfa69bc150ee16629e5.webpfile

          有volatile修飾的共享變量進(jìn)行寫操作的時(shí)候會(huì)多出第二行匯編代碼,也就是jvm會(huì)向處理器發(fā)送一條Lock前綴的指令,Lock前綴的指令在多核處理器下會(huì)引發(fā)兩件事情:

          1. 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存
          2. 這個(gè)寫回內(nèi)存的操作會(huì)使在其他CPU緩存了該內(nèi)存地址的數(shù)據(jù)無效,保證各個(gè)處理器的緩存是一致的 (通過一致性協(xié)議來實(shí)現(xiàn)的)

          一致性協(xié):每個(gè)處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己的緩存的值是否過期了,當(dāng)處理器發(fā)現(xiàn)自己的緩存行對(duì)應(yīng)的內(nèi)存過期,在下次訪問相同內(nèi)存地址時(shí),強(qiáng)制執(zhí)行緩存填充,從系統(tǒng)內(nèi)存中讀取。

          簡(jiǎn)單理解:volatile在其修飾的變量被線程修改時(shí),會(huì)強(qiáng)制其他線程在下一次訪問該變量時(shí)刷新緩存區(qū)。

          volatile的兩條實(shí)現(xiàn)原則

          1. Look 前綴指令會(huì)引起處理器緩存回寫到內(nèi)存。Lock 前綴指令導(dǎo)致在執(zhí)行指令期間,聲言處理器的LOCK#信號(hào)。在多處理器環(huán)境中,LOCK#信號(hào)確保在聲言該信號(hào)期間,處理器可以獨(dú)占任何共享內(nèi)存。但是,在最近的處理器里,LOCK#信號(hào)一般不鎖總線,而是鎖緩存,畢竟鎖總線開銷的比較大,對(duì)干intel486和Pentiuln處理器,在鎖操作時(shí),總是在總線上聲言LOCK#信號(hào)。但在P6和目前的處理器中,如果訪問的內(nèi)存區(qū)域已經(jīng)緩存在處理器內(nèi)部,則不會(huì)聲言LOCK#信號(hào)。相反,它會(huì)鎖定這塊內(nèi)存區(qū)域的緩存并回寫到內(nèi)存,并使用緩存一致性機(jī)制來確保修改的原子性,此操作被稱為“緩存鎖定”,緩存一致性機(jī)制會(huì)阻止同時(shí)修改由兩個(gè)以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù)。

          2. 一個(gè)處理器的緩存回寫到內(nèi)存會(huì)導(dǎo)致其他處理器的緩存無效。IA-32 處理器和 Iniel 64 處理器使用 MESI (修改、獨(dú)占、共享、無效)控制協(xié)議去維護(hù)內(nèi)部緩存和其他處理器緩存的一致性。在多核處理器系統(tǒng)中進(jìn)行操作的時(shí)候, IA-32和Intel64處理器能嗅探其他處理器訪問系統(tǒng)內(nèi)存和它們的內(nèi)部緩存。處理器使用嗅探技術(shù)保證它的內(nèi)部緩存、系統(tǒng)內(nèi)存和其他處理器的緩存的數(shù)據(jù)在總線上保持一致。例如,在 Pentium 和 P6famaly 處理器中,如果通過嗅探一個(gè)處理器來檢測(cè)其他處理器打算寫內(nèi)存地址,而這個(gè)地址當(dāng)前處干共享狀態(tài),那么正在嗅探的處理器將使它的緩存行無效,在下次訪問相同內(nèi)存地址時(shí),強(qiáng)制執(zhí)行緩存行填充

          小結(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)前最新值。

          volatile是如何保證有序性

          在解釋有序性前,我們先來看看什么是指令重排?

          導(dǎo)致程序有序性的原因是編譯優(yōu)化,指令重排序是JVM為了優(yōu)化指令,提高程序運(yùn)行效率,在不影響單線程程序執(zhí)行結(jié)果的前提下,盡可能地提高并行度。但是在多線程環(huán)境下,有些代碼的順序改變,有可能引發(fā)邏輯上的不正確。有序性最直接的辦法就是禁用緩存和編譯優(yōu)化,但是這樣問題雖然解決了,我們的程序的性能就堪憂了,所以合理的方案是按需禁用緩存或者編譯優(yōu)化。

          接下來我們來看一個(gè)著名的單例模式雙重檢查鎖的實(shí)現(xiàn)

          class Singleton {    private volatile static Singleton instance = null;    private Singleton() {    }    public static Singleton getInstance() {        if (instance == null) {                    //步驟1            synchronized (Singleton.class) {                if (instance == null)              //步驟2                    instance = new Singleton();    //步驟3            }        }        return instance;    }}

          在以上代碼中,instance不用volatile修飾時(shí),輸出的結(jié)果會(huì)是什么呢?我們的預(yù)期中代碼是這樣子執(zhí)行的:線程A和B同時(shí)在調(diào)用getInstance()方法,線程A執(zhí)行步驟1,發(fā)現(xiàn)instance為 null,然后同步鎖住Singleton類,接著執(zhí)行步驟2再次判斷instance是否為null,發(fā)現(xiàn)仍然是null,然后執(zhí)行步驟3,開始實(shí)例化Singleton。這樣看好像沒啥毛病,可是仔細(xì)一想,發(fā)現(xiàn)事情并不簡(jiǎn)單。?這時(shí)候,我們來我們先了解一下對(duì)象是怎么初始化的?

          • 對(duì)象在初始化的時(shí)候分三個(gè)步驟
          memory = allocate();   //1、分配對(duì)象的內(nèi)存空間ctorInstance(memory);  //2、初始化對(duì)象instance = memory;     //3、使instance指向?qū)ο蟮膬?nèi)存空間

          程序?yàn)榱藘?yōu)化性能,會(huì)將2和3進(jìn)行重排序,此時(shí)執(zhí)行的順序是1、3、2,在單線程中,對(duì)結(jié)果是不會(huì)有影響的,可是在多線程程序下,問題就暴露出來了。這時(shí)候我們回到剛剛的單例模式中,在實(shí)例化的過程中,線程B走到步驟1,發(fā)現(xiàn)instance不為空,但是有可能因?yàn)橹噶钪嘏帕?導(dǎo)致instance還沒有完全初始化,程序就出問題了。為了禁止實(shí)例化過程中的重排序,我們用volatile對(duì)instance修飾。908d901f0e16994f5f183cdb241fc952.webp

          volatile內(nèi)存語義如何實(shí)現(xiàn)

          對(duì)于一般的變量則會(huì)被重排序(重排序分析編譯器重排序和處理器重排序),而對(duì)于volatile則不能,這樣會(huì)影響其內(nèi)存語義,所以為了實(shí)現(xiàn)volatile的內(nèi)存語義JMM會(huì)限制重排序

          其重排序規(guī)則如下:
          4ee90a0acb29e69def591afbc829b11e.webp
          1. 如果第一個(gè)操作為volatile讀,則不管第二個(gè)操作是啥,都不能重排序。這個(gè)操作確保volatile讀之后的操作不會(huì)被編譯器重排序到volatile讀之前,其前面的所有普通寫操作都已經(jīng)刷新到主內(nèi)存中;

          2. 如果第一個(gè)操作volatile寫,不管第二個(gè)操作是volatile讀/寫,禁止重排序。

          3. 如果第二個(gè)操作為volatile寫時(shí),則不管第一個(gè)操作是啥,都不能重排序。這個(gè)操作確保volatile寫之前的操作不會(huì)被編譯器重排序到volatile寫之后;

          4. 如果第二個(gè)操作為volatile讀時(shí),不管第二個(gè)操作是volatile讀/寫,禁止重排序

          volatile的底層實(shí)現(xiàn)是通過插入內(nèi)存屏障,但是對(duì)于編譯器來說,發(fā)現(xiàn)一個(gè)最優(yōu)布置來最小化插入內(nèi)存屏障的總數(shù)幾乎是不可能的,所以,JMM采用了保守策略。如下:

          1. 在每一個(gè)volatile讀操作后面插入一個(gè)LoadLoad屏障,用來禁止處理器把上面的volatile讀與后面任意操作重排序

          2. 在每一個(gè)volatile寫操作前面插入一個(gè)StoreStore屏障,用來禁止volatile寫與前面任意操作重排序

          3. 在每一個(gè)volatile寫操作后面插入一個(gè)StoreLoad屏障,用來禁止volatile寫與后面可能有的volatile讀/寫操作重排序

          4. 在每一個(gè)volatile讀操作前面插入一個(gè)LoadStore屏障,用來禁止volatile寫與后面可能有的volatile讀/寫操作重排序

          保守策略下,volatile的寫插入屏障后生成的指令示意圖:

          72ab191ddd81b2f70779c77e65636df3.webp

          Storestore 屏障可以保證在volatile寫之前,其前面的所有普通寫操作已經(jīng)對(duì)任意處理器可見了,Storestore 屏障將保障上面所有的普通寫在volatile寫之前刷新到主內(nèi)存。

          這里比較有意思的是, volatite 寫后面的 StoreLoad 屏障的作用是避免volatile寫與后面可能有的volatile 讀/寫操作重排序。

          因?yàn)榫幾g器常常無法準(zhǔn)確判斷在一個(gè)volatile寫的后面是否需要插入一個(gè)StoreLoad屏障。為保證能正確實(shí)現(xiàn)volatile的內(nèi)存語義,JMM在采取了保守策略,在每個(gè)volatile寫的后面,或者在每個(gè) volatile讀的前面插入一個(gè)StoreLoad屏障。

          保守策略下,volatile的讀插入屏障后生成的指令示意圖:0c54333cc12ea9a709728a36f298fab4.webp

          上面的內(nèi)存屏障插入策略非常保守,在實(shí)際執(zhí)行中,只要不改變volatile寫-讀的內(nèi)存語義,編譯器可根據(jù)情況省略不必要的屏障

          舉個(gè)例子:

          public class Test {    int a ;    volatile int v1 = 1;    volatile int v2 = 2;    public  void readWrite(){        int i = v1;//第一個(gè)volatile讀        int j = v2;//第二個(gè)volatile讀        a = i+j://普通讀        v1 = i+1;//第一個(gè)volatile寫        v2 =j+2;//第二個(gè)volatile寫    }
          public synchronized void read(){ if(flag){ System.out.println("---i = " + i); } }}

          針對(duì)readWrite方法,編譯器在生成字節(jié)碼的時(shí)候可以做到如下的優(yōu)化:c07b11202e0f4453c649172169a5a085.webp

          注意:最后一個(gè)storeLoad屏障不能省略。因?yàn)榈诙€(gè)volatile寫之后,方法立即return,此時(shí)編譯器無法精準(zhǔn)判斷后面是否會(huì)有vaolatile讀或者寫。

          如何正確使用volatile變量

          在某些情況下,如果讀操作遠(yuǎn)遠(yuǎn)大于寫操作,volatile 變量可以提供優(yōu)于鎖的性能優(yōu)勢(shì)。

          可是volatile變量不是說用就能用的,它必須滿足兩個(gè)約束條件:

          • 對(duì)變量的寫操作不依賴于當(dāng)前值。
          • 該變量沒有包含在具有其他變量的不變式中。

          第一個(gè)條件的限制使volatile變量不能用作線程安全計(jì)數(shù)器。雖然?i++?看上去類似一個(gè)單獨(dú)操作,實(shí)際上它是一個(gè)讀取-修改-寫入三個(gè)步驟的組合操作,必須以原子方式執(zhí)行,而 volatile不能保證這種情況下的原子操作。正確的操作需要使i的值在操作期間保持不變,而volatile 變量無法做到這一點(diǎn)。

          volatile和synchronized區(qū)別

          1. volatile比synchronized執(zhí)行成本更低,因?yàn)樗?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">不會(huì)引起線程上下文的切換和調(diào)度
          2. volatile本質(zhì)是在告訴jvm當(dāng)前變量在寄存器(工作內(nèi)存)中的值是不確定的,需要從主存中讀取;synchronized則是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問該變量,其他線程被阻塞住。
          3. volatile只能用來修飾變量,而synchronized可以用來修飾變量、方法、和類。
          4. volatile可以實(shí)現(xiàn)變量的可見性,禁止重排序和單次讀/寫的原子性;而synchronized則可以變量的可見性,禁止重排序和原子性。
          5. volatile不會(huì)造成線程的阻塞;synchronized可能會(huì)造成線程的阻塞。
          6. volatile標(biāo)記的變量不會(huì)被編譯器優(yōu)化;synchronized標(biāo)記的變量可以被編譯器優(yōu)化。

          volatile和atomic原子類區(qū)別

          1. Volatile變量可以確保先行關(guān)系,即寫操作會(huì)發(fā)生在后續(xù)的讀操作之前

          2. 但是Volatile對(duì)復(fù)合操作不能保證原子性。例如用volatile修飾i變量,那么i++操作就不是原子性的。

          3. atomic原子類提供的atomic方法可以讓i++這種操作具有原子性,如getAndIncrement()方法會(huì)原子性的進(jìn)行增量操作把當(dāng)前值加一,其它數(shù)據(jù)類型和引用變量也可以進(jìn)行相似操作,但是atomic原子類一次只能操作一個(gè)共享變量,不能同時(shí)操作多個(gè)共享變量

          總結(jié)

          總結(jié)一下volatile的特性``

          • volatile可見性;對(duì)一個(gè)volatile的讀,總可以看到對(duì)這個(gè)變量最終的寫volatile有序性;JVM底層采用“內(nèi)存屏障”來實(shí)現(xiàn)volatile語義volatile原子性;volatile對(duì)單個(gè)讀/寫具有原子性(32位Long、Double),但是復(fù)合操作除外,例如i++
          文章參考來源:
          • 《并發(fā)編程的藝術(shù)》
          • 《并發(fā)編程實(shí)現(xiàn)》


          ?如果你覺得文章還不錯(cuò),你的轉(zhuǎn)發(fā)、分享、贊賞、點(diǎn)贊、留言就是對(duì)我最大的鼓勵(lì)。感謝您的閱讀!

          ?原創(chuàng)不易,歡迎轉(zhuǎn)發(fā),求個(gè)關(guān)注,文末右下角賞個(gè)"在看"吧。

          c25e249ced96308a8bd824d9e80d3d54.webp

          往期精選

          上一篇:原創(chuàng)|《菜鳥讀并發(fā)》多線程問題必知必會(huì)概念

          下一篇:原創(chuàng)|《菜鳥讀并發(fā)》多線程程序如何調(diào)試?

          碼農(nóng)進(jìn)階之路

          長(zhǎng)按二維碼關(guān)注?

          面經(jīng) | 原理?| 源碼 | 實(shí)戰(zhàn) | 工具

          點(diǎn)擊留言010d3eb87b07d6822112061e675bf2dc.webp
          瀏覽 21
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲一级簧片 | 免费作爱视频 | 搡老熟女大熟了88AV一区二区 | 免费一级黄色毛片 | 一级a毛片免费观看久久精品 |