一文解決記憶體屏障

monkeysayhi發表於2018-01-08

記憶體屏障是硬體之上、作業系統或JVM之下,對併發作出的最後一層支援。再向下是是硬體提供的支援;向上是作業系統或JVM對記憶體屏障作出的各種封裝。記憶體屏障是一種標準,各廠商可能採用不同的實現。

本文僅為了幫助理解JVM提供的併發機制。首先,從volatile的語義引出可見性與重排序問題;接下來,闡述問題的產生原理,瞭解為什麼需要記憶體屏障;然後,淺談記憶體屏障的標準、廠商對記憶體屏障的支援,並以volatile為例討論記憶體屏障如何解決這些問題;最後,補充介紹JVM在記憶體屏障之上作出的幾個封裝。為了幫助理解,會簡要討論硬體架構層面的一些基本原理(特別是CPU架構),但不會深入實現機制。

記憶體屏障的實現涉及大量硬體架構層面的知識,又需要作業系統或JVM的配合才能發揮威力,單純從任何一個層面都無法理解。本文整合了這三個層面的大量知識,篇幅較長,希望能在一篇文章內,把記憶體屏障的基本問題講述清楚。

如有疏漏,還望指正!

volatile變數規則

一個用於引出記憶體屏障的好例子是volatile變數規則

volatile關鍵字可參考猴子剛開部落格時的文章volatile關鍵字的作用、原理。volatile變數規則描述了volatile變數的偏序語義;這裡從volatile變數規則的角度來講解,順便做個複習。

定義

volatile變數規則:對volatile變數的寫入操作必須在對該變數的讀操作之前執行

volatile變數規則只是一種標準,要求JVM實現保證volatile變數的偏序語義。結合程式順序規則、傳遞性,該偏序語義通常表現為兩個作用:

  • 保持可見性
  • 禁用重排序(讀操作禁止重排序之後的操作,寫操作禁止重排序之前的操作)

補充:

  • 程式順序規則:如果程式中操作A在操作B之前,那麼線上程中操作A將在操作B之前執行。
  • 傳遞性:如果操作A在操作B之前執行,並且操作B在操作C之前執行,那麼操作A必須在操作C之前執行。

後文,如果僅涉及可見性,則指明“可見性”;如果二者均涉及,則以“偏序”代稱。重排序一定會帶來可見性問題,因此,不會出現單獨討論重排序的場景。

正確姿勢

之前的文章多次涉及volatile變數規則的用法。

簡單的僅利用volatile變數規則對volatile變數本身的可見性保證:

複雜的利用volatile變數規則(結合了程式順序規則、傳遞性)保證變數本身及周圍其他變數的偏序:

可見性與重排序

前文多次提到可見性與重排序的問題,記憶體屏障的存在就是為了解決這些問題。到底什麼是可見性?什麼是重排序?為什麼會有這些問題?

可見性

定義

可見性的定義常見於各種併發場景中,以多執行緒為例:當一個執行緒修改了執行緒共享變數的值,其它執行緒能夠立即得知這個修改。

從效能角度考慮,沒有必要在修改後就立即同步修改的值——如果多次修改後才使用,那麼只需要最後一次同步即可,在這之前的同步都是效能浪費。因此,實際的可見性定義要弱一些,只需要保證:當一個執行緒修改了執行緒共享變數的值,其它執行緒在使用前,能夠得到最新的修改值

可見性可以認為是最弱的“一致性”(弱一致),只保證使用者見到的資料是一致的,但不保證任意時刻,儲存的資料都是一致的(強一致)。下文會討論“快取可見性”問題,部分文章也會稱為“快取一致性”問題。

問題來源

一個最簡單的可見性問題來自計算機內部的快取架構:

image.png

快取大大縮小了高速CPU與低速記憶體之間的差距。以三層快取架構為例:

  • L1 Cache最接近CPU, 容量最小(如32K、64K等)、速度最高,每個核上都有一個L1 Cache。
  • L2 Cache容量更大(如256K)、速度更低, 一般情況下,每個核上都有一個獨立的L2 Cache。
  • L3 Cache最接近記憶體,容量最大(如12MB),速度最低,在同一個CPU插槽之間的核共享一個L3 Cache。

準確地說,每個核上有兩個L1 Cache, 一個存資料 L1d Cache, 一個存指令 L1i Cache。

單核時代的一切都是那麼完美。然而,多核時代出現了可見性問題。一個badcase如下:

  1. Core0與Core1命中了記憶體中的同一個地址,那麼各自的L1 Cache會快取同一份資料的副本。
  2. 最開始,Core0與Core1都在友善的讀取這份資料。
  3. 突然,Core0要使壞了,它修改了這份資料,使得兩份快取中的資料不同了,更確切的說,Core1 L1 Cache中的資料失效了。

單核時代只有Core0,Core0修改Core0讀,沒什麼問題;但是,現在_Core0修改後,Core1並不知道資料已經失效,繼續傻傻的使用_,輕則資料計算錯誤,重則導致死迴圈、程式崩潰等。

實際的可見性問題還要擴充套件到兩個方向:

  • 除三級快取外,各廠商實現的硬體架構中還存在多種多樣的快取,都存在類似的可見性問題。例如,暫存器就相當於CPU與L1 Cache之間的快取。
  • 各種高階語言(包括Java)的多執行緒記憶體模型中,線上程棧內自己維護一份快取是常見的優化措施,但顯然在CPU級別的快取可見性問題面前,一切都失效了

以上只是最簡單的可見性問題,不涉及重排序等。

重排序也會導致可見性問題;同時,快取上的可見性也會引起一些看似重排序導致的問題。

重排序

定義

重排序並沒有嚴格的定義。整體上可以分為兩種:

  • 真·重排序:編譯器、底層硬體(CPU等)出於“優化”的目的,按照某種規則將指令重新排序(儘管有時候看起來像亂序)。
  • 偽·重排序:由於快取同步順序等問題,看起來指令被重排序了。

重排序也是單核時代非常優秀的優化手段,有足夠多的措施保證其在單核下的正確性。在多核時代,如果工作執行緒之間不共享資料或僅共享不可變資料,重排序也是效能優化的利器。然而,如果工作執行緒之間共享了可變資料,由於兩種重排序的結果都不是固定的,會導致工作執行緒似乎表現出了隨機行為。

第一次接觸重排序的概念一定很迷糊,耐心,耐心。

問題來源

重排序問題無時無刻不在發生,源自三種場景:

  1. 編譯器編譯時的優化
  2. 處理器執行時的亂序優化
  3. 快取同步順序(導致可見性問題)

場景1、2屬於真·重排序;場景3屬於偽·重排序。場景3也屬於可見性問題,為保持連貫性,我們先討論場景3。

可見性導致的偽·重排序

快取同步順序本質上是可見性問題。

假設程式順序(program order)中先更新變數v1、再更新變數v2,不考慮真·重排序:

  1. Core0先更新快取中的v1,再更新快取中的v2(位於兩個快取行,這樣淘汰快取行時不會一起寫回記憶體)。
  2. Core0讀取v1(假設使用LRU協議淘汰快取)。
  3. Core0的快取滿,將最遠使用的v2寫回記憶體。
  4. Core1的快取中本來存有v1,現在將v2載入入快取。

重排序是針對程式順序而言的,如果指令執行順序與程式順序不同,就說明這段指令被重排序了。

此時,儘管“更新v1”的事件早於“更新v2”發生,但Core1只看到了v2的最新值,卻看不到v1的最新值。這屬於可見性導致的偽·重排序:雖然沒有實際上沒有重排序,但看起來發生了重排序

可以看到,快取可見性不僅僅導致可見性問題,還會導致偽·重排序。因此,只要解決了快取上的可見性問題,也就解決了偽·重排序

MESI協議

回到可見性問題中的例子和可見性的定義。要解決這個問題很簡單,套用可見性的定義,只需要:在Core0修改了資料v後,讓Core1在使用v前,能得到v最新的修改值

這個要求很弱,既可以在每次修改v後,都同步修改值到其他快取了v的Cache中;又可以只同步使用前的最後一次修改值。後者效能上更優,如何實現呢:

  1. Core0修改v後,傳送一個訊號,將Core1快取的v標記為失效,並將修改值寫回記憶體。
  2. Core0可能會多次修改v,每次修改都只傳送一個訊號(發訊號時會鎖住快取間的匯流排),Core1快取的v保持著失效標記。
  3. Core1使用v前,發現快取中的v已經失效了,得知v已經被修改了,於是重新從其他快取或記憶體中載入v。

以上即是MESI(Modified Exclusive Shared Or Invalid,快取的四種狀態)協議的基本原理,不算嚴謹,但對於理解快取可見性(更常見的稱呼是“快取一致性”)已經足夠。

MESI協議解決了CPU快取層面的可見性問題。

以下是MESI協議的快取狀態機,簡單看看即可:

image.png

狀態:

  • M(修改, Modified): 本地處理器已經修改快取行, 即是髒行, 它的內容與記憶體中的內容不一樣. 並且此cache只有本地一個拷貝(專有)。
  • E(專有, Exclusive): 快取行內容和記憶體中的一樣, 而且其它處理器都沒有這行資料。
  • S(共享, Shared): 快取行內容和記憶體中的一樣, 有可能其它處理器也存在此快取行的拷貝。
  • I(無效, Invalid): 快取行失效, 不能使用。
剩餘問題

既然有了MESI協議,是不是就不需要volatile的可見性語義了?當然不是,還有三個問題:

  • 並不是所有的硬體架構都提供了相同的一致性保證,JVM需要volatile統一語義(就算是MESI,也只解決CPU快取層面的問題,沒有涉及其他層面)。
  • 可見性問題不僅僅侷限於CPU快取內,JVM自己維護的記憶體模型中也有可見性問題。使用volatile做標記,可以解決JVM層面的可見性問題。
  • 如果不考慮真·重排序,MESI確實解決了CPU快取層面的可見性問題;然而,真·重排序也會導致可見性問題。

暫時第一個問題稱為“記憶體可見性”問題,記憶體屏障解決了該問題。後文討論。

編譯器編譯時的優化

JVM自己維護的記憶體模型中也有可見性問題,使用volatile做標記,取消volatile變數的快取,就解決了JVM層面的可見性問題。編譯器產生的重排序也採用了同樣的思路。

編譯器為什麼要重排序(re-order)呢?和處理器亂序執行的目的是一樣的:與其等待阻塞指令(如等待快取刷入)完成,不如先去執行其他指令。與處理器亂序執行相比,編譯器重排序能夠完成更大範圍、效果更好的亂序優化。

由於同處理器亂序執行的目的相同,原理相似,這裡不討論編譯器重排序的實現原理。

幸運的是,既然是編譯器層面的重排序,自然可以由編譯器控制。使用volatile做標記,就可以禁用編譯器層面的重排序。

處理器執行時的亂序優化

處理器層面的亂序優化節省了大量等待時間,提高了處理器的效能。

所謂“亂序”只是被叫做“亂序”,實際上也遵循著一定規則:只要兩個指令之間不存在資料依賴,就可以對這兩個指令亂序。不必關心資料依賴的精確定義,可以理解為:只要不影響程式單執行緒、順序執行的結果,就可以對兩個指令重排序

不進行亂序優化時,處理器的指令執行過程如下:

  1. 指令獲取。
  2. 如果輸入的運算物件是可以獲取的(比如已經存在於暫存器中),這條指令會被髮送到合適的功能單元。如果一個或者更多的運算物件在當前的時鐘週期中是不可獲取的(通常需要從主記憶體獲取),處理器會開始等待直到它們是可以獲取的。
  3. 指令在合適的功能單元中被執行。
  4. 功能單元將運算結果寫回暫存器。

亂序優化下的執行過程如下:

  1. 指令獲取。
  2. 指令被髮送到一個指令序列(也稱執行緩衝區或者保留站)中。
  3. 指令將在序列中等待,直到它的資料運算物件是可以獲取的。然後,指令被允許在先進入的、舊的指令之前離開序列緩衝區。(此處表現為亂序)
  4. 指令被分配給一個合適的功能單元並由之執行。
  5. 結果被放到一個序列中。
  6. 僅當所有在該指令之前的指令都將他們的結果寫入暫存器後,這條指令的結果才會被寫入暫存器中。(重整亂序結果)

當然,為了實現亂序優化,還需要很多技術的支援,如暫存器重新命名分枝預測等,但大致瞭解到這裡就足夠。後文的註釋中會據此給出記憶體屏障的實現方案。

亂序優化在單核時代不影響正確性;但多核時代的多執行緒能夠在不同的核上實現真正的並行,一旦執行緒間共享資料,就出現問題了。看一段很經典的程式碼:

public class OutofOrderExecution {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;
    
    public static void main(String[] args)
        throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            public void run() {
                a = 1;
                x = b;
            }
        });
        Thread t2 = new Thread(new Runnable() {
            public void run() {
                b = 1;
                y = a;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(“(” + x + “,” + y + “)”);
    }
}
複製程式碼

不考慮編譯器重排序和快取可見性問題,上面的程式碼可能會輸出什麼呢?

最容易想到的結果是(0,1)(1,0)(1,1)。因為可能先後執行執行緒t1、t2,也可能反之,還可能t1、t2交替執行。

然而,這段程式碼的執行結果也可能是(0,0),看起來違反常理。這是處理器亂序執行的結果:執行緒t1內部的兩行程式碼之間不存在資料依賴,因此,可以將x = b亂序到a = 1前;同時,執行緒t2中的y = a早於執行緒t1中的a = 1執行。一個可能的執行序列如下:

  1. t1: x = b
  2. t2: b = 1
  3. t2: y = a
  4. t1: a = 1

這裡將程式碼等同於指令,不嚴謹,但不妨礙理解。

看起來,似乎將上述重排序(或亂序)導致的問題稱為“可見性”問題也未嘗不可。然而,這種重排序的危害要遠遠大於單純的可見性,因為並不是所有的指令都是簡單的讀或者寫——面試中單例模式有幾種寫法?volatile關鍵字的作用、原理中都提到了部分初始化的例子,這種不安全釋出就是由於重排序導致的。因此,將重排序歸為“可見性”問題並不合適,只能說重排序會導致可見性問題

也就是說,單純解決記憶體可見性問題是不夠的,還需要專門解決處理器重排序的問題

當然,某些處理器不會對指令亂序,或能夠基於多核間的資料依賴亂序。這時,volatile僅用於統一重排序方面的語義。

記憶體屏障

記憶體屏障(Memory Barrier)與記憶體柵欄(Memory Fence)是同一個概念,不同的叫法。

通過volatile標記,可以解決編譯器層面的可見性與重排序問題。而記憶體屏障則解決了硬體層面的可見性與重排序問題

猴子暫時沒有驗證下述分析,僅從邏輯和系統設計考量上進行了判斷、取捨。以後會補上實驗。

標準

先簡單瞭解兩個指令:

  • Store:將處理器快取的資料重新整理到記憶體中。
  • Load:將記憶體儲存的資料拷貝到處理器的快取中。
屏障型別 指令示例 說明
LoadLoad Barriers Load1;LoadLoad;Load2 該屏障確保Load1資料的裝載先於Load2及其後所有裝載指令的的操作
StoreStore Barriers Store1;StoreStore;Store2 該屏障確保Store1立刻重新整理資料到記憶體(使其對其他處理器可見)的操作先於Store2及其後所有儲存指令的操作
LoadStore Barriers Load1;LoadStore;Store2 確保Load1的資料裝載先於Store2及其後所有的儲存指令重新整理資料到記憶體的操作
StoreLoad Barriers Store1;StoreLoad;Load2 該屏障確保Store1立刻重新整理資料到記憶體的操作先於Load2及其後所有裝載裝載指令的操作。它會使該屏障之前的所有記憶體訪問指令(儲存指令和訪問指令)完成之後,才執行該屏障之後的記憶體訪問指令

StoreLoad Barriers同時具備其他三個屏障的效果,因此也稱之為全能屏障(mfence),是目前大多數處理器所支援的;但是相對其他屏障,該屏障的開銷相對昂貴。

然而,除了mfence,不同的CPU架構對記憶體屏障的實現方式與實現程度非常不一樣。相對來說,Intel CPU的強記憶體模型比DEC Alpha的弱複雜記憶體模型(快取不僅分層了,還分割槽了)更簡單。x86架構是在多執行緒程式設計中最常見的,下面討論x86架構中記憶體屏障的實現。

查閱資料時,你會發現每篇講記憶體屏障的文章講的都不同。不過,重要的是理解基本原理,需要的時候再繼續深究即可。

不過不管是那種方案,記憶體屏障的實現都要針對亂序執行的過程來設計。前文的註釋中講解了亂序執行的基本原理:核心是一個序列緩衝區,只要指令的資料運算物件是可以獲取的,指令就被允許在先進入的、舊的指令之前離開序列緩衝區,開始執行。對於記憶體可見性的語義,記憶體屏障可以通過使用類似MESI協議的思路實現。對於重排序語義的實現機制,猴子沒有繼續研究,一種可行的思路是:

  • 當CPU收到屏障指令時,不將屏障指令放入序列緩衝區,而將屏障指令及後續所有指令放入一個FIFO佇列中(指令是按批傳送的,不然沒有亂序的必要)
  • 允許亂序執行完序列緩衝區中的所有指令
  • 從FIFO佇列中取出屏障指令,執行(並重新整理快取等,實現記憶體可見性的語義)
  • 將FIFO佇列中的剩餘指令放入序列緩衝區
  • 恢復正常的亂序執行

對於x86架構中的sfence屏障指令而言,則保證sfence之前的store執行完,再執行sfence,最後執行sfence之後的store;除了禁用sfence前後store亂序帶來的新的資料依賴外,不影響load命令的亂序。詳細見後。

x86架構的記憶體屏障

x86架構並沒有實現全部的記憶體屏障。

Store Barrier

sfence指令實現了Store Barrier,相當於StoreStore Barriers。

強制所有在sfence指令之前的store指令,都在該sfence指令執行之前被執行,傳送快取失效訊號,並把store buffer中的資料刷出到CPU的L1 Cache中;所有在sfence指令之後的store指令,都在該sfence指令執行之後被執行。即,禁止對sfence指令前後store指令的重排序跨越sfence指令,使所有Store Barrier之前發生的記憶體更新都是可見的

這裡的“可見”,指修改值可見(記憶體可見性)且操作結果可見(禁用重排序)。下同。

記憶體屏障的標準中,討論的是快取與記憶體間的相干性,實際上,同樣適用於暫存器與快取、甚至暫存器與記憶體間等多級快取之間。x86架構使用了MESI協議的一個變種,由協議保證三層快取與記憶體間的相關性,則記憶體屏障只需要保證store buffer(可以認為是暫存器與L1 Cache間的一層快取)與L1 Cache間的相干性。下同。

Load Barrier

lfence指令實現了Load Barrier,相當於LoadLoad Barriers。

強制所有在lfence指令之後的load指令,都在該lfence指令執行之後被執行,並且一直等到load buffer被該CPU讀完才能執行之後的load指令(發現快取失效後發起的刷入)。即,禁止對lfence指令前後load指令的重排序跨越lfence指令,配合Store Barrier,使所有Store Barrier之前發生的記憶體更新,對Load Barrier之後的load操作都是可見的

Full Barrier

mfence指令實現了Full Barrier,相當於StoreLoad Barriers。

mfence指令綜合了sfence指令與lfence指令的作用,強制所有在mfence指令之前的store/load指令,都在該mfence指令執行之前被執行;所有在mfence指令之後的store/load指令,都在該mfence指令執行之後被執行。即,禁止對mfence指令前後store/load指令的重排序跨越mfence指令,使所有Full Barrier之前發生的操作,對所有Full Barrier之後的操作都是可見的。

volatile如何解決記憶體可見性與處理器重排序問題

在編譯器層面,僅將volatile作為標記使用,取消編譯層面的快取和重排序。

如果硬體架構本身已經保證了記憶體可見性(如單核處理器、一致性足夠的記憶體模型等),那麼volatile就是一個空標記,不會插入相關語義的記憶體屏障。

如果硬體架構本身不進行處理器重排序、有更強的重排序語義(能夠分析多核間的資料依賴)、或在單核處理器上重排序,那麼volatile就是一個空標記,不會插入相關語義的記憶體屏障。

如果不保證,仍以x86架構為例,JVM對volatile變數的處理如下:

  • 在寫volatile變數v之後,插入一個sfence。這樣,sfence之前的所有store(包括寫v)不會被重排序到sfence之後,sfence之後的所有store不會被重排序到sfence之前,禁用跨sfence的store重排序;且sfence之前修改的值都會被寫回快取,並標記其他CPU中的快取失效。
  • 在讀volatile變數v之前,插入一個lfence。這樣,lfence之後的load(包括讀v)不會被重排序到lfence之前,lfence之前的load不會被重排序到lfence之後,禁用跨lfence的load重排序;且lfence之後,會首先重新整理無效快取,從而得到最新的修改值,與sfence配合保證記憶體可見性。

在另外一些平臺上,JVM使用mfence代替sfence與lfence,實現更強的語義。

二者結合,共同實現了Happens-Before關係中的volatile變數規則。

JVM對記憶體屏障作出的其他封裝

除volatile外,常見的JVM實現還基於記憶體屏障作了一些其他封裝。藉助於記憶體屏障,這些封裝也得到了記憶體屏障在可見性與重排序上的語義

藉助:piggyback。

在JVM中,藉助通常指:將Happens-Before的程式順序規則與其他某個順序規則(通常是監視器鎖規則、volatile變數規則)結合起來,從而對某個未被鎖保護的變數的訪問操作進行排序。

本文將藉助的語義擴充套件到更大的範圍,可以藉助任何現有機制,以獲得現有機制的某些屬性。當然,並不是所有屬性都能被藉助,比如原子性。但基於前文對記憶體屏障的分析可知,可見性與重排序是可以被藉助的。

下面仍基於x86架構討論。

final關鍵字

如果一個例項的欄位被宣告為final,則JVM會在初始化final變數後插入一個sfence。

類的final欄位在<clinit>()方法中初始化,其可見性由JVM的類載入過程保證。

final欄位的初始化在<init>()方法中完成。sfence禁用了sfence前後對store的重排序,且保證final欄位初始化之前(include)的記憶體更新都是可見的。

再談部分初始化

上述良好性質被稱為“初始化安全性”。它保證,對於被正確構造的物件,所有執行緒都能看到建構函式給物件的各個final欄位設定的正確值,而不管採用何種方式來發布物件

這裡將可見性從“final欄位初始化之前(include)的記憶體更新”縮小到“final欄位初始化”。猴子沒找到確切的原因,手裡暫時只有一個jdk也不方便驗證。可能是因為,JVM沒有要求虛擬機器實現在生成<init>()方法時編排欄位初始化指令的順序

初始化安全性為解決部分初始化問題帶來了新的思路:如果待發布物件的所有域都是final修飾的,那麼可以防止對物件的初始引用被重排序到構造過程完成之前。於是,面試中單例模式有幾種寫法?中的飽漢變種三還可以扔掉volatile,改為藉助final的sfence語義:

// 飽漢
// ThreadSafe
public class Singleton1_3 {
  private static Singleton1_3 singleton = null;
  
  public int f1 = 1;   // 觸發部分初始化問題
  public int f2 = 2;

  private Singleton1_3() {
  }

  public static Singleton1_3 getInstance() {
    if (singleton == null) {
      synchronized (Singleton1_3.class) {
        // must be a complete instance
        if (singleton == null) {
          singleton = new Singleton1_3();
        }
      }
    }
    return singleton;
  }
}
複製程式碼

注意,初始化安全性僅針對安全釋出中的部分初始化問題,與其他安全釋出問題、釋出後的可見性問題無關。

CAS

在x86架構上,CAS被翻譯為"lock cmpxchg..."。cmpxchg是CAS的彙編指令。在CPU架構中依靠lock訊號保證可見性並禁止重排序。

lock字首是一個特殊的訊號,執行過程如下:

  • 對匯流排和快取上鎖。
  • 強制所有lock訊號之前的指令,都在此之前被執行,並同步相關快取。
  • 執行lock後的指令(如cmpxchg)。
  • 釋放對匯流排和快取上的鎖。
  • 強制所有lock訊號之後的指令,都在此之後被執行,並同步相關快取。

因此,lock訊號雖然不是記憶體屏障,但具有mfence的語義(當然,還有排他性的語義)。

與記憶體屏障相比,lock訊號要額外對匯流排和快取上鎖,成本更高。

JVM的內建鎖通過作業系統的管程實現。且不論管程的實現原理,由於管程是一種互斥資源,修改互斥資源至少需要一個CAS操作。因此,鎖必然也使用了lock訊號,具有mfence的語義。

鎖的mfence語義實現了Happens-Before關係中的監視器鎖規則。

CAS具有同樣的mfence語義,也必然具有與鎖相同的偏序關係。儘管JVM沒有對此作出顯式的要求。


參考:


本文連結:一文解決記憶體屏障 作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。

相關文章