1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        volatile 三部曲之可見性

        共 3935字,需瀏覽 8分鐘

         ·

        2021-04-06 14:51


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

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

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

        可見性

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

        LOCK

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

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

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

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

         

        JMM

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

        volatile

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

        此時線程 2 并沒有讀到線程 1 寫入的最新值,a=2,而是讀到了主內存中的舊值,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 關鍵字
        比如在一個變量 a 前面加上了 volatile 關鍵字
        volatile int a;
        那么在寫這個 volatile 變量時,JMM 會把該線程對應的本地內存中的共享變量值立即刷新到主內存。
        相應地,當讀一個 volatile 變量時,JMM 會把該線程對應的本地內存置為無效。線程接下來將從主內存中讀取共享變量。
        以上兩點,就是 volatile 的內存語義。
        而這兩點的實質上,是完成了一次線程間通信,即線程 1 向線程 2 發(fā)送了消息。
        有的同學可能又要問了,內存語義,那真的是寫的時候刷新到主內存,而讀的時候讓本地內存失效么?
        這里我還是要強調,JMM 是語言級的內存模型,無論它硬件層面上是怎么去保證的,在你站在語言層面去學習 JMM 時,就不要去想硬件細節(jié)。
        為了解決部分同學的困惑,我還是用不準確的語言來說一下,volatile 的底層會被轉化成上面所說的 LOCK 指令,寫這個共享變量時,就既做了刷新到主內存,同時也將其他處理器緩存失效的操作,并不是寫的時候刷新緩存,讀的時候再去將本地內存失效。
        但在語言層去描述 volatile 的內存語義時,剛剛的說法完全沒錯,只要程序員按照 JMM 這個內存模型和 volatile 的內存語義去編程,能夠方便理解,且能夠達到預期的效果,即可。至于是不是準確表達了硬件層面的原理,這個是不重要的。
        這讓我想到了之前看過的一個演講,我記得叫“眼見為實”,是說我們看到的,并不一定是這個宇宙的真實面貌,只是能讓我們更好地生存并延續(xù)后代,而已。


        后記





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

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

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

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

        瀏覽 30
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

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

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            男女黄色视频在线观看 | 丁香五月激情婷婷综合 | 美女搞逼视频 | 人人妻人人澡人人爽久久av | 99国产超薄肉色丝袜交足的后果 | 婷婷狠狠爱 | 韩国成人网站在线观看 | 狠狠艹综合 | 操我好爽 | 男男惩罚往屁股眼里夹姜 |