<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>

          volatile 三部曲之可見性

          共 3935字,需瀏覽 8分鐘

           ·

          2021-04-06 14:51


          低并發(fā)編程
          戰(zhàn)略上藐視技術(shù),戰(zhàn)術(shù)上重視技術(shù)

          先給自己挖個坑,三部曲如下:
          volatile 三部曲之可見性
          volatile 三部曲之有序性
          volatile 三部曲之經(jīng)典應(yīng)用
          今天講可見性,廢話不多說,開始。
          友情提示:本文基于 Java 語言,CPU 基于 x86 架構(gòu)。

          有一個內(nèi)存,在其 0x400 位置處,存儲著數(shù)字 1
          有一個處理器,從內(nèi)存中讀數(shù)據(jù)到寄存器時,會將讀到的數(shù)據(jù)在緩存中存儲一份。
          現(xiàn)在,這個處理器讀取到了三條機器指令,將內(nèi)存中的數(shù)字改寫為了 2
          我們看到,這個寫的過程被細(xì)化成了兩步,需要先寫到處理器緩存,再從緩存刷新到內(nèi)存。
          同樣對于讀來說,也需要先讀緩存,如果讀不到再去內(nèi)存中獲取,同時更新緩存。
          這樣,對于單個處理器來說,由于緩存的存在,讀寫效率都有所提升。
           

          可見性

           
          可是,如果有另一個處理器呢?
          場景一:處理器 1 未及時將緩存中的值刷新到內(nèi)存,導(dǎo)致處理器 2 讀到了內(nèi)存中的舊值。
          場景二:處理器 1 及時刷新緩存到了內(nèi)存,但處理器 2 讀的是自己緩存中的舊值。
          可以看到,這兩種場景,都是處理器 1 認(rèn)為,已經(jīng)將共享變量改寫為了 2,但處理器 2 讀到的值仍然是 1。
          換句話說,處理器 1 對這個共享變量的修改,對處理器 2 來說"不可見"。
          現(xiàn)在我們加入線程的概念,假設(shè)線程 1 運行在處理器 1,線程 2 運行在處理器 2。
          那么就可以說:
          線程 1 對這個共享變量的修改,對線程 2 來說"不可見"。
          這個問題,就被稱為可見性問題。
           

          LOCK

           
          假如線程 1 對共享變量的修改,線程 2 立刻就能夠看到。
          那么就可以說,這個共享變量,具有可見性。
          那如何做到這一點呢?
          我們首先想想看,剛剛的兩個場景,為什么不可見。

          1. 線程 1 對共享變量的修改,如果剛剛將其值寫入自己的緩存,卻還沒有刷新到內(nèi)存,此時內(nèi)存的值仍為舊值。

          2. 即使線程 1 將其修改后的值,從緩存刷新到了內(nèi)存,但線程 2 仍然從自己的緩存中讀取,讀到的也可能是舊值。

          所以,問題就出在這兩個地方。
          那要解決這個問題也非常簡單,只需要在線程 1 將共享變量進行寫操作時,產(chǎn)生如下兩個效果即可。
          1. 線程 1 將新值寫入緩存后,立刻刷新到內(nèi)存中。
          2. 這個寫入內(nèi)存的操作,使線程 2 的緩存無效。若想讀取該共享變量,則需要重新從內(nèi)存中獲取。
          這樣,該共享變量,就具有了可見性。
          那如何使得,一個線程在進行寫操作時,有上述兩個效果呢?
          答案是 LOCK 指令。
          假如,線程 1 執(zhí)行了如下指令,將內(nèi)存中某地址處的值+1。
          add [某內(nèi)存地址], 1
          現(xiàn)在這個寫操作,不會立即刷新到內(nèi)存,也不會將其他處理器中的緩存失效,也即不具備可見性。
          那只需要加上一個 LOCK 前綴。
          lock add [某內(nèi)存地址], 1
          這樣,這個操作就會使得:
          1. 立即將該處理器緩存(具體說是緩存行)中的數(shù)據(jù)刷新到內(nèi)存。
          2. 使得其他處理器緩存(具體說是緩存了該內(nèi)存地址的緩存行)失效。
          第一步將緩存刷新到內(nèi)存后,使得其他處理器緩存失效,也就是第二步的發(fā)生,是利用了 CPU 的緩存一致性協(xié)議。
          而為了實現(xiàn)緩存一致性協(xié)議,每個處理器通常的一個做法是,通過監(jiān)聽在總線上傳播的數(shù)據(jù)來判斷自己的緩存值是否過期,這種方式叫總線嗅探機制。
          總之,這兩個效果一出,在程序員或者線程的眼中,就變成了可見性的保證。

           

          JMM

           
          現(xiàn)在,讓我們來到 Java 語言的世界。
          上面那些處理器、寄存器、緩存等,都是硬件層面的概念,如果把這些無聊的、難學(xué)的細(xì)節(jié),暴露給程序員,估計 Java 就無法流行起來了吧。
          Java 可不希望這種情況發(fā)生,于是發(fā)明了一個簡單的、抽象的內(nèi)存模型,來屏蔽這些硬件層面的細(xì)節(jié)。
          這個內(nèi)存模型就叫做 JMM,Java Memory Module。
          一個線程寫入一個共享變量時,需要先寫入自己的本地內(nèi)存,再刷新到主內(nèi)存。默認(rèn)情況下,JMM 并不會保證什么時候刷新到主內(nèi)存。
          同樣,一個線程讀一個共享變量時,需要先讀取自己的本地內(nèi)存,如果讀不到再去主內(nèi)存中讀取,同時更新到自己的本地內(nèi)存。
          有同學(xué)就要問了,這個本地內(nèi)存,是在內(nèi)存中開辟的一塊空間么?一個線程讀一個內(nèi)存中的數(shù)據(jù),還需要從內(nèi)存一個地方拷貝到另一個地方?
          為啥上面有個×?因為怕有的人把這個圖當(dāng)成正解了...
          注意,JMM 是語言級的內(nèi)存模型,所以你千萬不能把這個模型中的概念,同真實的硬件層的概念相關(guān)聯(lián),這也是很多同學(xué)對此感到迷惑的根源。
          JMM 的出現(xiàn),就是為了讓程序員不要去想硬件上的細(xì)節(jié),但這樣的命名方式,反而使程序員理解起來更加困惑了。
          如果非要對應(yīng)硬件上的原理,那不準(zhǔn)確地說,這里的本地內(nèi)存實際上在并不真實存在,是由于處理器中的緩存機制而產(chǎn)生的抽象概念。這么說可能稍稍解決你的一點點困惑。
          之所以說不準(zhǔn)確,一是因為處理器有很多不同的架構(gòu),并不一定所有的架構(gòu)都有緩存。二是因為除了緩存之外,還有其他硬件和編譯器的優(yōu)化,可以導(dǎo)致本地內(nèi)存這個概念的存在。
          所以從某種程度上說,JMM 還確實是大大簡化和屏蔽了程序員對于硬件細(xì)節(jié)的了解。
           

          volatile

           
          根據(jù) JMM 向程序員提供的抽象模型,我們可以推測出如下問題。

          此時線程 2 并沒有讀到線程 1 寫入的最新值,a=2,而是讀到了主內(nèi)存中的舊值,a=1。
          也即,線程 1 對共享變量的寫入,對線程 2 不可見。
          那么在 Java 中,如何讓一個共享變量具有上述的可見性呢?
          答案是加一個 volatile 即可。
          在 jls 里是這樣描述 volatile 的。
          The Java programming language allows threads to access shared variables. As a rule, to ensure that shared variables are consistently and reliably updated, a thread should ensure that it has exclusive use of such variables by obtaining a lock that, conventionally, enforces mutual exclusion for those shared variables.
          The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes.
          簡單說,Java 語言為了確保共享變量得到一致和可靠的更新,可以通過鎖,也可以通過更輕量的 volatile 關(guān)鍵字
          比如在一個變量 a 前面加上了 volatile 關(guān)鍵字
          volatile int a;
          那么在寫這個 volatile 變量時,JMM 會把該線程對應(yīng)的本地內(nèi)存中的共享變量值立即刷新到主內(nèi)存。
          相應(yīng)地,當(dāng)讀一個 volatile 變量時,JMM 會把該線程對應(yīng)的本地內(nèi)存置為無效。線程接下來將從主內(nèi)存中讀取共享變量。
          以上兩點,就是 volatile 的內(nèi)存語義。
          而這兩點的實質(zhì)上,是完成了一次線程間通信,即線程 1 向線程 2 發(fā)送了消息。
          有的同學(xué)可能又要問了,內(nèi)存語義,那真的是寫的時候刷新到主內(nèi)存,而讀的時候讓本地內(nèi)存失效么?
          這里我還是要強調(diào),JMM 是語言級的內(nèi)存模型,無論它硬件層面上是怎么去保證的,在你站在語言層面去學(xué)習(xí) JMM 時,就不要去想硬件細(xì)節(jié)。
          為了解決部分同學(xué)的困惑,我還是用不準(zhǔn)確的語言來說一下,volatile 的底層會被轉(zhuǎn)化成上面所說的 LOCK 指令,寫這個共享變量時,就既做了刷新到主內(nèi)存,同時也將其他處理器緩存失效的操作,并不是寫的時候刷新緩存,讀的時候再去將本地內(nèi)存失效。
          但在語言層去描述 volatile 的內(nèi)存語義時,剛剛的說法完全沒錯,只要程序員按照 JMM 這個內(nèi)存模型和 volatile 的內(nèi)存語義去編程,能夠方便理解,且能夠達(dá)到預(yù)期的效果,即可。至于是不是準(zhǔn)確表達(dá)了硬件層面的原理,這個是不重要的。
          這讓我想到了之前看過的一個演講,我記得叫“眼見為實”,是說我們看到的,并不一定是這個宇宙的真實面貌,只是能讓我們更好地生存并延續(xù)后代,而已。


          后記





          寫這篇文章時真的是瑟瑟發(fā)抖,一是因為網(wǎng)上講這個知識點的實在太多了,二是我發(fā)現(xiàn) volatile 這個知識點水很深,從底層硬件一直到上層語言,每一層都有實現(xiàn)原理,層層抽象直到上層表現(xiàn)為我們看到的樣子。

          我甚至覺得不可能有人對這個知識點完全理解透徹。緩存一致性和總線嗅探,你需要了解 CPU 硬件的原理吧?JMM 內(nèi)存模型,你需要了解 JVM 虛擬機實現(xiàn)吧?

          或者不說實現(xiàn)的事兒,就單單是 JMM 說了什么,很多人覺得懂了,但你看過 JSR133 文檔對 JMM 模型的正式規(guī)范么?很長,給大家隨便截取一小段。

          所以隨著不斷研究這個知識點,我發(fā)現(xiàn)我越來越不懂 volatile 了。
          但我還是寫下了這篇文章,并給自己挖了個坑。
          這篇文章我盡全力把網(wǎng)上一些混亂的概念講解,重新理清楚,且盡量把和可見性無關(guān)的東西去掉。
          但我還是寫的很不滿意,也很郁悶。
          因為我覺得,我離 volatile 的真相,還很遙遠(yuǎn)。

          瀏覽 30
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  欧美一级免费观看视频 | 高清无码在线观看丁香五月婷婷 | 丁香五月天婷婷婷 | 99在线精品视频观看 | 成人四房播播 |