一文解決內(nèi)存屏障

內(nèi)存屏障是硬件之上、操作系統(tǒng)或JVM之下例隆,對并發(fā)作出的最后一層支持甥捺。再向下是是硬件提供的支持;向上是操作系統(tǒng)或JVM對內(nèi)存屏障作出的各種封裝镀层。內(nèi)存屏障是一種標準镰禾,各廠商可能采用不同的實現(xiàn)。

本文僅為了幫助理解JVM提供的并發(fā)機制唱逢。首先吴侦,從volatile的語義引出可見性與重排序問題;接下來坞古,闡述問題的產(chǎn)生原理备韧,了解為什么需要內(nèi)存屏障;然后痪枫,淺談內(nèi)存屏障的標準织堂、廠商對內(nèi)存屏障的支持,并以volatile為例討論內(nèi)存屏障如何解決這些問題奶陈;最后易阳,補充介紹JVM在內(nèi)存屏障之上作出的幾個封裝。為了幫助理解吃粒,會簡要討論硬件架構(gòu)層面的一些基本原理(特別是CPU架構(gòu))潦俺,但不會深入實現(xiàn)機制。

內(nèi)存屏障的實現(xiàn)涉及大量硬件架構(gòu)層面的知識声搁,又需要操作系統(tǒng)或JVM的配合才能發(fā)揮威力黑竞,單純從任何一個層面都無法理解。本文整合了這三個層面的大量知識疏旨,篇幅較長很魂,希望能在一篇文章內(nèi),把內(nèi)存屏障的基本問題講述清楚檐涝。

如有疏漏遏匆,還望指正法挨!

volatile變量規(guī)則

一個用于引出內(nèi)存屏障的好例子是volatile變量規(guī)則

volatile關(guān)鍵字可參考猴子剛開博客時的文章volatile關(guān)鍵字的作用幅聘、原理凡纳。volatile變量規(guī)則描述了volatile變量的偏序語義;這里從volatile變量規(guī)則的角度來講解帝蒿,順便做個復(fù)習(xí)荐糜。

定義

volatile變量規(guī)則:對volatile變量的寫入操作必須在對該變量的讀操作之前執(zhí)行

volatile變量規(guī)則只是一種標準葛超,要求JVM實現(xiàn)保證volatile變量的偏序語義暴氏。結(jié)合程序順序規(guī)則、傳遞性绣张,該偏序語義通常表現(xiàn)為兩個作用:

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

補充:

  • 程序順序規(guī)則:如果程序中操作A在操作B之前,那么在線程中操作A將在操作B之前執(zhí)行侥涵。
  • 傳遞性:如果操作A在操作B之前執(zhí)行沼撕,并且操作B在操作C之前執(zhí)行,那么操作A必須在操作C之前執(zhí)行芜飘。

后文务豺,如果僅涉及可見性,則指明“可見性”燃箭;如果二者均涉及冲呢,則以“偏序”代稱。重排序一定會帶來可見性問題招狸,因此敬拓,不會出現(xiàn)單獨討論重排序的場景。

正確姿勢

之前的文章多次涉及volatile變量規(guī)則的用法裙戏。

簡單的僅利用volatile變量規(guī)則對volatile變量本身的可見性保證:

復(fù)雜的利用volatile變量規(guī)則(結(jié)合了程序順序規(guī)則、傳遞性)保證變量本身及周圍其他變量的偏序:

可見性與重排序

前文多次提到可見性與重排序的問題赂蠢,內(nèi)存屏障的存在就是為了解決這些問題。到底什么是可見性辨泳?什么是重排序虱岂?為什么會有這些問題玖院?

可見性

定義

可見性的定義常見于各種并發(fā)場景中,以多線程為例:當一個線程修改了線程共享變量的值第岖,其它線程能夠立即得知這個修改难菌。

從性能角度考慮,沒有必要在修改后就立即同步修改的值——如果多次修改后才使用蔑滓,那么只需要最后一次同步即可郊酒,在這之前的同步都是性能浪費。因此烫饼,實際的可見性定義要弱一些猎塞,只需要保證:當一個線程修改了線程共享變量的值试读,其它線程在使用前杠纵,能夠得到最新的修改值

可見性可以認為是最弱的“一致性”(弱一致)钩骇,只保證用戶見到的數(shù)據(jù)是一致的比藻,但不保證任意時刻,存儲的數(shù)據(jù)都是一致的(強一致)倘屹。下文會討論“緩存可見性”問題银亲,部分文章也會稱為“緩存一致性”問題。

問題來源

一個最簡單的可見性問題來自計算機內(nèi)部的緩存架構(gòu):

image.png

緩存大大縮小了高速CPU與低速內(nèi)存之間的差距纽匙。以三層緩存架構(gòu)為例:

  • L1 Cache最接近CPU, 容量最形耱稹(如32K、64K等)烛缔、速度最高馏段,每個核上都有一個L1 Cache。
  • L2 Cache容量更大(如256K)践瓷、速度更低, 一般情況下院喜,每個核上都有一個獨立的L2 Cache。
  • L3 Cache最接近內(nèi)存晕翠,容量最大(如12MB)喷舀,速度最低,在同一個CPU插槽之間的核共享一個L3 Cache淋肾。

準確地說硫麻,每個核上有兩個L1 Cache, 一個存數(shù)據(jù) L1d Cache, 一個存指令 L1i Cache。

單核時代的一切都是那么完美樊卓。然而拿愧,多核時代出現(xiàn)了可見性問題。一個badcase如下:

  1. Core0與Core1命中了內(nèi)存中的同一個地址简识,那么各自的L1 Cache會緩存同一份數(shù)據(jù)的副本赶掖。
  2. 最開始感猛,Core0與Core1都在友善的讀取這份數(shù)據(jù)。
  3. 突然奢赂,Core0要使壞了陪白,它修改了這份數(shù)據(jù),使得兩份緩存中的數(shù)據(jù)不同了膳灶,更確切的說咱士,Core1 L1 Cache中的數(shù)據(jù)失效了。

單核時代只有Core0轧钓,Core0修改Core0讀序厉,沒什么問題;但是毕箍,現(xiàn)在Core0修改后弛房,Core1并不知道數(shù)據(jù)已經(jīng)失效,繼續(xù)傻傻的使用而柑,輕則數(shù)據(jù)計算錯誤文捶,重則導(dǎo)致死循環(huán)、程序崩潰等媒咳。

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

  • 除三級緩存外粹排,各廠商實現(xiàn)的硬件架構(gòu)中還存在多種多樣的緩存,都存在類似的可見性問題涩澡。例如顽耳,寄存器就相當于CPU與L1 Cache之間的緩存。
  • 各種高級語言(包括Java)的多線程內(nèi)存模型中妙同,在線程棧內(nèi)自己維護一份緩存是常見的優(yōu)化措施射富,但顯然在CPU級別的緩存可見性問題面前,一切都失效了渐溶。

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

重排序也會導(dǎo)致可見性問題茎辐;同時宪郊,緩存上的可見性也會引起一些看似重排序?qū)е碌膯栴}。

重排序

定義

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

  • 真·重排序:編譯器弛槐、底層硬件(CPU等)出于“優(yōu)化”的目的,按照某種規(guī)則將指令重新排序(盡管有時候看起來像亂序)依啰。
  • 偽·重排序:由于緩存同步順序等問題乎串,看起來指令被重排序了。

重排序也是單核時代非常優(yōu)秀的優(yōu)化手段速警,有足夠多的措施保證其在單核下的正確性叹誉。在多核時代鸯两,如果工作線程之間不共享數(shù)據(jù)或僅共享不可變數(shù)據(jù),重排序也是性能優(yōu)化的利器长豁。然而钧唐,如果工作線程之間共享了可變數(shù)據(jù),由于兩種重排序的結(jié)果都不是固定的匠襟,會導(dǎo)致工作線程似乎表現(xiàn)出了隨機行為钝侠。

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

問題來源

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

  1. 編譯器編譯時的優(yōu)化
  2. 處理器執(zhí)行時的亂序優(yōu)化
  3. 緩存同步順序(導(dǎo)致可見性問題)

場景1啃勉、2屬于真·重排序忽舟;場景3屬于偽·重排序。場景3也屬于可見性問題璧亮,為保持連貫性萧诫,我們先討論場景3。

可見性導(dǎo)致的偽·重排序

緩存同步順序本質(zhì)上是可見性問題枝嘶。

假設(shè)程序順序(program order)中先更新變量v1、再更新變量v2哑诊,不考慮真·重排序:

  1. Core0先更新緩存中的v1群扶,再更新緩存中的v2(位于兩個緩存行,這樣淘汰緩存行時不會一起寫回內(nèi)存)镀裤。
  2. Core0讀取v1(假設(shè)使用LRU協(xié)議淘汰緩存)竞阐。
  3. Core0的緩存滿,將最遠使用的v2寫回內(nèi)存暑劝。
  4. Core1的緩存中本來存有v1骆莹,現(xiàn)在將v2加載入緩存。

重排序是針對程序順序而言的担猛,如果指令執(zhí)行順序與程序順序不同幕垦,就說明這段指令被重排序了。

此時傅联,盡管“更新v1”的事件早于“更新v2”發(fā)生先改,但Core1只看到了v2的最新值,卻看不到v1的最新值蒸走。這屬于可見性導(dǎo)致的偽·重排序:雖然沒有實際上沒有重排序仇奶,但看起來發(fā)生了重排序

可以看到比驻,緩存可見性不僅僅導(dǎo)致可見性問題该溯,還會導(dǎo)致偽·重排序岛抄。因此,只要解決了緩存上的可見性問題狈茉,也就解決了偽·重排序弦撩。

MESI協(xié)議

回到可見性問題中的例子和可見性的定義。要解決這個問題很簡單论皆,套用可見性的定義益楼,只需要:在Core0修改了數(shù)據(jù)v后,讓Core1在使用v前点晴,能得到v最新的修改值感凤。

這個要求很弱,既可以在每次修改v后粒督,都同步修改值到其他緩存了v的Cache中陪竿;又可以只同步使用前的最后一次修改值。后者性能上更優(yōu)屠橄,如何實現(xiàn)呢:

  1. Core0修改v后族跛,發(fā)送一個信號,將Core1緩存的v標記為失效锐墙,并將修改值寫回內(nèi)存礁哄。
  2. Core0可能會多次修改v,每次修改都只發(fā)送一個信號(發(fā)信號時會鎖住緩存間的總線)溪北,Core1緩存的v保持著失效標記桐绒。
  3. Core1使用v前,發(fā)現(xiàn)緩存中的v已經(jīng)失效了之拨,得知v已經(jīng)被修改了茉继,于是重新從其他緩存或內(nèi)存中加載v。

以上即是MESI(Modified Exclusive Shared Or Invalid蚀乔,緩存的四種狀態(tài))協(xié)議的基本原理烁竭,不算嚴謹,但對于理解緩存可見性(更常見的稱呼是“緩存一致性”)已經(jīng)足夠吉挣。

MESI協(xié)議解決了CPU緩存層面的可見性問題派撕。

以下是MESI協(xié)議的緩存狀態(tài)機,簡單看看即可:

image.png

狀態(tài):

  • M(修改, Modified): 本地處理器已經(jīng)修改緩存行, 即是臟行, 它的內(nèi)容與內(nèi)存中的內(nèi)容不一樣. 并且此cache只有本地一個拷貝(專有)听想。
  • E(專有, Exclusive): 緩存行內(nèi)容和內(nèi)存中的一樣, 而且其它處理器都沒有這行數(shù)據(jù)腥刹。
  • S(共享, Shared): 緩存行內(nèi)容和內(nèi)存中的一樣, 有可能其它處理器也存在此緩存行的拷貝。
  • I(無效, Invalid): 緩存行失效, 不能使用汉买。
剩余問題

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

  • 并不是所有的硬件架構(gòu)都提供了相同的一致性保證垫卤,JVM需要volatile統(tǒng)一語義(就算是MESI威彰,也只解決CPU緩存層面的問題,沒有涉及其他層面)穴肘。
  • 可見性問題不僅僅局限于CPU緩存內(nèi)歇盼,JVM自己維護的內(nèi)存模型中也有可見性問題。使用volatile做標記评抚,可以解決JVM層面的可見性問題豹缀。
  • 如果不考慮真·重排序,MESI確實解決了CPU緩存層面的可見性問題慨代;然而邢笙,真·重排序也會導(dǎo)致可見性問題。

暫時第一個問題稱為“內(nèi)存可見性”問題侍匙,內(nèi)存屏障解決了該問題氮惯。后文討論。

編譯器編譯時的優(yōu)化

JVM自己維護的內(nèi)存模型中也有可見性問題想暗,使用volatile做標記妇汗,取消volatile變量的緩存,就解決了JVM層面的可見性問題说莫。編譯器產(chǎn)生的重排序也采用了同樣的思路杨箭。

編譯器為什么要重排序(re-order)呢?和處理器亂序執(zhí)行的目的是一樣的:與其等待阻塞指令(如等待緩存刷入)完成唬滑,不如先去執(zhí)行其他指令告唆。與處理器亂序執(zhí)行相比,編譯器重排序能夠完成更大范圍晶密、效果更好的亂序優(yōu)化。

由于同處理器亂序執(zhí)行的目的相同模她,原理相似稻艰,這里不討論編譯器重排序的實現(xiàn)原理。

幸運的是侈净,既然是編譯器層面的重排序尊勿,自然可以由編譯器控制。使用volatile做標記畜侦,就可以禁用編譯器層面的重排序元扔。

處理器執(zhí)行時的亂序優(yōu)化

處理器層面的亂序優(yōu)化節(jié)省了大量等待時間,提高了處理器的性能旋膳。

所謂“亂序”只是被叫做“亂序”澎语,實際上也遵循著一定規(guī)則:只要兩個指令之間不存在數(shù)據(jù)依賴,就可以對這兩個指令亂序。不必關(guān)心數(shù)據(jù)依賴的精確定義擅羞,可以理解為:只要不影響程序單線程尸变、順序執(zhí)行的結(jié)果,就可以對兩個指令重排序减俏。

不進行亂序優(yōu)化時召烂,處理器的指令執(zhí)行過程如下:

  1. 指令獲取。
  2. 如果輸入的運算對象是可以獲取的(比如已經(jīng)存在于寄存器中)娃承,這條指令會被發(fā)送到合適的功能單元奏夫。如果一個或者更多的運算對象在當前的時鐘周期中是不可獲取的(通常需要從主內(nèi)存獲取)历筝,處理器會開始等待直到它們是可以獲取的酗昼。
  3. 指令在合適的功能單元中被執(zhí)行。
  4. 功能單元將運算結(jié)果寫回寄存器漫谷。

亂序優(yōu)化下的執(zhí)行過程如下:

  1. 指令獲取仔雷。
  2. 指令被發(fā)送到一個指令序列(也稱執(zhí)行緩沖區(qū)或者保留站)中。
  3. 指令將在序列中等待舔示,直到它的數(shù)據(jù)運算對象是可以獲取的碟婆。然后,指令被允許在先進入的惕稻、舊的指令之前離開序列緩沖區(qū)竖共。(此處表現(xiàn)為亂序)
  4. 指令被分配給一個合適的功能單元并由之執(zhí)行。
  5. 結(jié)果被放到一個序列中俺祠。
  6. 僅當所有在該指令之前的指令都將他們的結(jié)果寫入寄存器后公给,這條指令的結(jié)果才會被寫入寄存器中。(重整亂序結(jié)果)

當然蜘渣,為了實現(xiàn)亂序優(yōu)化,還需要很多技術(shù)的支持蔫缸,如寄存器重命名腿准、分枝預(yù)測等,但大致了解到這里就足夠拾碌。后文的注釋中會據(jù)此給出內(nèi)存屏障的實現(xiàn)方案吐葱。

亂序優(yōu)化在單核時代不影響正確性;但多核時代的多線程能夠在不同的核上實現(xiàn)真正的并行校翔,一旦線程間共享數(shù)據(jù)弟跑,就出現(xiàn)問題了》乐ⅲ看一段很經(jīng)典的代碼:

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 + “)”);
    }
}

不考慮編譯器重排序和緩存可見性問題孟辑,上面的代碼可能會輸出什么呢哎甲?

最容易想到的結(jié)果是(0,1)(1,0)(1,1)扑浸。因為可能先后執(zhí)行線程t1烧给、t2,也可能反之喝噪,還可能t1础嫡、t2交替執(zhí)行。

然而酝惧,這段代碼的執(zhí)行結(jié)果也可能是(0,0)榴鼎,看起來違反常理。這是處理器亂序執(zhí)行的結(jié)果:線程t1內(nèi)部的兩行代碼之間不存在數(shù)據(jù)依賴,因此,可以將x = b亂序到a = 1前净赴;同時,線程t2中的y = a早于線程t1中的a = 1執(zhí)行平项。一個可能的執(zhí)行序列如下:

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

這里將代碼等同于指令,不嚴謹悍及,但不妨礙理解闽瓢。

看起來,似乎將上述重排序(或亂序)導(dǎo)致的問題稱為“可見性”問題也未嘗不可心赶。然而扣讼,這種重排序的危害要遠遠大于單純的可見性,因為并不是所有的指令都是簡單的讀或者寫——面試中單例模式有幾種寫法缨叫?volatile關(guān)鍵字的作用椭符、原理中都提到了部分初始化的例子,這種不安全發(fā)布就是由于重排序?qū)е碌某芾选R虼耍?strong>將重排序歸為“可見性”問題并不合適销钝,只能說重排序會導(dǎo)致可見性問題。

也就是說琐簇,單純解決內(nèi)存可見性問題是不夠的曙搬,還需要專門解決處理器重排序的問題

當然鸽嫂,某些處理器不會對指令亂序,或能夠基于多核間的數(shù)據(jù)依賴亂序征讲。這時据某,volatile僅用于統(tǒng)一重排序方面的語義。

內(nèi)存屏障

內(nèi)存屏障(Memory Barrier)與內(nèi)存柵欄(Memory Fence)是同一個概念诗箍,不同的叫法癣籽。

通過volatile標記,可以解決編譯器層面的可見性與重排序問題。而內(nèi)存屏障則解決了硬件層面的可見性與重排序問題筷狼。

猴子暫時沒有驗證下述分析瓶籽,僅從邏輯和系統(tǒng)設(shè)計考量上進行了判斷、取舍埂材。以后會補上實驗塑顺。

標準

先簡單了解兩個指令:

  • Store:將處理器緩存的數(shù)據(jù)刷新到內(nèi)存中。
  • Load:將內(nèi)存存儲的數(shù)據(jù)拷貝到處理器的緩存中俏险。
屏障類型 指令示例 說明
LoadLoad Barriers Load1;LoadLoad;Load2 該屏障確保Load1數(shù)據(jù)的裝載先于Load2及其后所有裝載指令的的操作
StoreStore Barriers Store1;StoreStore;Store2 該屏障確保Store1立刻刷新數(shù)據(jù)到內(nèi)存(使其對其他處理器可見)的操作先于Store2及其后所有存儲指令的操作
LoadStore Barriers Load1;LoadStore;Store2 確保Load1的數(shù)據(jù)裝載先于Store2及其后所有的存儲指令刷新數(shù)據(jù)到內(nèi)存的操作
StoreLoad Barriers Store1;StoreLoad;Load2 該屏障確保Store1立刻刷新數(shù)據(jù)到內(nèi)存的操作先于Load2及其后所有裝載裝載指令的操作严拒。它會使該屏障之前的所有內(nèi)存訪問指令(存儲指令和訪問指令)完成之后,才執(zhí)行該屏障之后的內(nèi)存訪問指令

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

然而种蘸,除了mfence,不同的CPU架構(gòu)對內(nèi)存屏障的實現(xiàn)方式與實現(xiàn)程度非常不一樣竞膳。相對來說航瞭,Intel CPU的強內(nèi)存模型比DEC Alpha的弱復(fù)雜內(nèi)存模型(緩存不僅分層了,還分區(qū)了)更簡單顶猜。x86架構(gòu)是在多線程編程中最常見的沧奴,下面討論x86架構(gòu)中內(nèi)存屏障的實現(xiàn)。

查閱資料時长窄,你會發(fā)現(xiàn)每篇講內(nèi)存屏障的文章講的都不同滔吠。不過,重要的是理解基本原理挠日,需要的時候再繼續(xù)深究即可疮绷。

不過不管是那種方案,內(nèi)存屏障的實現(xiàn)都要針對亂序執(zhí)行的過程來設(shè)計嚣潜。前文的注釋中講解了亂序執(zhí)行的基本原理:核心是一個序列緩沖區(qū)冬骚,只要指令的數(shù)據(jù)運算對象是可以獲取的,指令就被允許在先進入的懂算、舊的指令之前離開序列緩沖區(qū)只冻,開始執(zhí)行。對于內(nèi)存可見性的語義计技,內(nèi)存屏障可以通過使用類似MESI協(xié)議的思路實現(xiàn)喜德。對于重排序語義的實現(xiàn)機制,猴子沒有繼續(xù)研究垮媒,一種可行的思路是:

  • 當CPU收到屏障指令時舍悯,不將屏障指令放入序列緩沖區(qū)航棱,而將屏障指令及后續(xù)所有指令放入一個FIFO隊列中(指令是按批發(fā)送的,不然沒有亂序的必要)
  • 允許亂序執(zhí)行完序列緩沖區(qū)中的所有指令
  • 從FIFO隊列中取出屏障指令萌衬,執(zhí)行(并刷新緩存等饮醇,實現(xiàn)內(nèi)存可見性的語義)
  • 將FIFO隊列中的剩余指令放入序列緩沖區(qū)
  • 恢復(fù)正常的亂序執(zhí)行

對于x86架構(gòu)中的sfence屏障指令而言,則保證sfence之前的store執(zhí)行完秕豫,再執(zhí)行sfence朴艰,最后執(zhí)行sfence之后的store;除了禁用sfence前后store亂序帶來的新的數(shù)據(jù)依賴外馁蒂,不影響load命令的亂序呵晚。詳細見后。

x86架構(gòu)的內(nèi)存屏障

x86架構(gòu)并沒有實現(xiàn)全部的內(nèi)存屏障沫屡。

Store Barrier

sfence指令實現(xiàn)了Store Barrier饵隙,相當于StoreStore Barriers。

強制所有在sfence指令之前的store指令沮脖,都在該sfence指令執(zhí)行之前被執(zhí)行金矛,發(fā)送緩存失效信號,并把store buffer中的數(shù)據(jù)刷出到CPU的L1 Cache中勺届;所有在sfence指令之后的store指令驶俊,都在該sfence指令執(zhí)行之后被執(zhí)行。即免姿,禁止對sfence指令前后store指令的重排序跨越sfence指令饼酿,使所有Store Barrier之前發(fā)生的內(nèi)存更新都是可見的

這里的“可見”胚膊,指修改值可見(內(nèi)存可見性)且操作結(jié)果可見(禁用重排序)故俐。下同。

內(nèi)存屏障的標準中紊婉,討論的是緩存與內(nèi)存間的相干性药版,實際上,同樣適用于寄存器與緩存喻犁、甚至寄存器與內(nèi)存間等多級緩存之間槽片。x86架構(gòu)使用了MESI協(xié)議的一個變種,由協(xié)議保證三層緩存與內(nèi)存間的相關(guān)性肢础,則內(nèi)存屏障只需要保證store buffer(可以認為是寄存器與L1 Cache間的一層緩存)與L1 Cache間的相干性还栓。下同。

Load Barrier

lfence指令實現(xiàn)了Load Barrier传轰,相當于LoadLoad Barriers蝙云。

強制所有在lfence指令之后的load指令,都在該lfence指令執(zhí)行之后被執(zhí)行路召,并且一直等到load buffer被該CPU讀完才能執(zhí)行之后的load指令(發(fā)現(xiàn)緩存失效后發(fā)起的刷入)勃刨。即,禁止對lfence指令前后load指令的重排序跨越lfence指令股淡,配合Store Barrier身隐,使所有Store Barrier之前發(fā)生的內(nèi)存更新,對Load Barrier之后的load操作都是可見的唯灵。

Full Barrier

mfence指令實現(xiàn)了Full Barrier贾铝,相當于StoreLoad Barriers。

mfence指令綜合了sfence指令與lfence指令的作用埠帕,強制所有在mfence指令之前的store/load指令垢揩,都在該mfence指令執(zhí)行之前被執(zhí)行;所有在mfence指令之后的store/load指令敛瓷,都在該mfence指令執(zhí)行之后被執(zhí)行叁巨。即,禁止對mfence指令前后store/load指令的重排序跨越mfence指令呐籽,使所有Full Barrier之前發(fā)生的操作锋勺,對所有Full Barrier之后的操作都是可見的。

volatile如何解決內(nèi)存可見性與處理器重排序問題

在編譯器層面狡蝶,僅將volatile作為標記使用庶橱,取消編譯層面的緩存和重排序。

如果硬件架構(gòu)本身已經(jīng)保證了內(nèi)存可見性(如單核處理器贪惹、一致性足夠的內(nèi)存模型等)苏章,那么volatile就是一個空標記,不會插入相關(guān)語義的內(nèi)存屏障奏瞬。

如果硬件架構(gòu)本身不進行處理器重排序枫绅、有更強的重排序語義(能夠分析多核間的數(shù)據(jù)依賴)、或在單核處理器上重排序丝格,那么volatile就是一個空標記撑瞧,不會插入相關(guān)語義的內(nèi)存屏障。

如果不保證显蝌,仍以x86架構(gòu)為例预伺,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配合保證內(nèi)存可見性征炼。

在另外一些平臺上析既,JVM使用mfence代替sfence與lfence,實現(xiàn)更強的語義谆奥。

二者結(jié)合眼坏,共同實現(xiàn)了Happens-Before關(guān)系中的volatile變量規(guī)則。

JVM對內(nèi)存屏障作出的其他封裝

除volatile外雄右,常見的JVM實現(xiàn)還基于內(nèi)存屏障作了一些其他封裝空骚。借助于內(nèi)存屏障,這些封裝也得到了內(nèi)存屏障在可見性與重排序上的語義擂仍。

借助:piggyback囤屹。

在JVM中,借助通常指:將Happens-Before的程序順序規(guī)則與其他某個順序規(guī)則(通常是監(jiān)視器鎖規(guī)則逢渔、volatile變量規(guī)則)結(jié)合起來肋坚,從而對某個未被鎖保護的變量的訪問操作進行排序。

本文將借助的語義擴展到更大的范圍肃廓,可以借助任何現(xiàn)有機制智厌,以獲得現(xiàn)有機制的某些屬性。當然盲赊,并不是所有屬性都能被借助铣鹏,比如原子性。但基于前文對內(nèi)存屏障的分析可知哀蘑,可見性與重排序是可以被借助的诚卸。

下面仍基于x86架構(gòu)討論。

final關(guān)鍵字

如果一個實例的字段被聲明為final绘迁,則JVM會在初始化final變量后插入一個sfence合溺。

類的final字段在<clinit>()方法中初始化,其可見性由JVM的類加載過程保證缀台。

final字段的初始化在<init>()方法中完成棠赛。sfence禁用了sfence前后對store的重排序,且保證final字段初始化之前(include)的內(nèi)存更新都是可見的。

再談部分初始化

上述良好性質(zhì)被稱為“初始化安全性”睛约。它保證鼎俘,對于被正確構(gòu)造的對象,所有線程都能看到構(gòu)造函數(shù)給對象的各個final字段設(shè)置的正確值痰腮,而不管采用何種方式來發(fā)布對象而芥。

這里將可見性從“final字段初始化之前(include)的內(nèi)存更新”縮小到“final字段初始化”。猴子沒找到確切的原因膀值,手里暫時只有一個jdk也不方便驗證∥蠹可能是因為沧踏,JVM沒有要求虛擬機實現(xiàn)在生成<init>()方法時編排字段初始化指令的順序

初始化安全性為解決部分初始化問題帶來了新的思路:如果待發(fā)布對象的所有域都是final修飾的巾钉,那么可以防止對對象的初始引用被重排序到構(gòu)造過程完成之前翘狱。于是,面試中單例模式有幾種寫法砰苍?中的飽漢變種三還可以扔掉volatile潦匈,改為借助final的sfence語義:

// 飽漢
// ThreadSafe
public class Singleton1_3 {
  private static Singleton1_3 singleton = null;
  
  public int f1 = 1;   // 觸發(fā)部分初始化問題
  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;
  }
}

注意,初始化安全性僅針對安全發(fā)布中的部分初始化問題赚导,與其他安全發(fā)布問題茬缩、發(fā)布后的可見性問題無關(guān)。

CAS

在x86架構(gòu)上吼旧,CAS被翻譯為"lock cmpxchg..."凰锡。cmpxchg是CAS的匯編指令。在CPU架構(gòu)中依靠lock信號保證可見性并禁止重排序圈暗。

lock前綴是一個特殊的信號掂为,執(zhí)行過程如下:

  • 對總線和緩存上鎖。
  • 強制所有l(wèi)ock信號之前的指令员串,都在此之前被執(zhí)行勇哗,并同步相關(guān)緩存。
  • 執(zhí)行l(wèi)ock后的指令(如cmpxchg)寸齐。
  • 釋放對總線和緩存上的鎖欲诺。
  • 強制所有l(wèi)ock信號之后的指令,都在此之后被執(zhí)行访忿,并同步相關(guān)緩存瞧栗。

因此,lock信號雖然不是內(nèi)存屏障海铆,但具有mfence的語義(當然迹恐,還有排他性的語義)。

與內(nèi)存屏障相比卧斟,lock信號要額外對總線和緩存上鎖殴边,成本更高憎茂。

JVM的內(nèi)置鎖通過操作系統(tǒng)的管程實現(xiàn)。且不論管程的實現(xiàn)原理锤岸,由于管程是一種互斥資源竖幔,修改互斥資源至少需要一個CAS操作。因此是偷,鎖必然也使用了lock信號拳氢,具有mfence的語義。

鎖的mfence語義實現(xiàn)了Happens-Before關(guān)系中的監(jiān)視器鎖規(guī)則蛋铆。

CAS具有同樣的mfence語義馋评,也必然具有與鎖相同的偏序關(guān)系。盡管JVM沒有對此作出顯式的要求刺啦。


參考:


本文鏈接:一文解決內(nèi)存屏障
作者:猴子007
出處:https://monkeysayhi.github.io
本文基于 知識共享署名-相同方式共享 4.0 國際許可協(xié)議發(fā)布糊渊,歡迎轉(zhuǎn)載右核,演繹或用于商業(yè)目的,但是必須保留本文的署名及鏈接再来。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蒙兰,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子芒篷,更是在濱河造成了極大的恐慌搜变,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件针炉,死亡現(xiàn)場離奇詭異挠他,居然都是意外死亡,警方通過查閱死者的電腦和手機篡帕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門殖侵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人镰烧,你說我怎么就攤上這事拢军。” “怎么了怔鳖?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵茉唉,是天一觀的道長。 經(jīng)常有香客問我,道長度陆,這世上最難降的妖魔是什么艾凯? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮懂傀,結(jié)果婚禮上趾诗,老公的妹妹穿的比我還像新娘。我一直安慰自己蹬蚁,他們只是感情好恃泪,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著犀斋,像睡著了一般悟泵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上闪水,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機與錄音蒙具,去河邊找鬼球榆。 笑死,一個胖子當著我的面吹牛禁筏,可吹牛的內(nèi)容都是我干的持钉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼篱昔,長吁一口氣:“原來是場噩夢啊……” “哼每强!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起州刽,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤空执,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后穗椅,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辨绊,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年匹表,在試婚紗的時候發(fā)現(xiàn)自己被綠了门坷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡袍镀,死狀恐怖默蚌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情苇羡,我是刑警寧澤绸吸,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響惯裕,放射性物質(zhì)發(fā)生泄漏温数。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一蜻势、第九天 我趴在偏房一處隱蔽的房頂上張望撑刺。 院中可真熱鬧,春花似錦握玛、人聲如沸够傍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽冕屯。三九已至,卻和暖如春拂苹,著一層夾襖步出監(jiān)牢的瞬間安聘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工瓢棒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留浴韭,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓脯宿,卻偏偏與公主長得像念颈,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子连霉,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容