深入理解并發(fā)內(nèi)存模型JMM與內(nèi)存屏障

(1)多核并發(fā)緩存架構(gòu)



早期計算機先把數(shù)據(jù)(硬盤數(shù)據(jù))加載到主內(nèi)存左敌,然后CPU再到內(nèi)存中取。由于現(xiàn)在CPU發(fā)展很快,CPU的運算速度比主內(nèi)存高得多瓦宜,為了避免受主內(nèi)存讀取速度的影響,所以現(xiàn)在會在CPU中有CPU緩存岭妖,速度接近CPU临庇,比主內(nèi)存快得多反璃,只要數(shù)據(jù)在CPU緩存,那么CPU的速度就沒有太大的限制假夺,發(fā)揮到最大



L1淮蜈、L2、L3就是CPU的高速緩存

(2)JMM內(nèi)存模型



Java多線程內(nèi)存模型跟CPU緩存模型類似已卷,是基于cpu緩存模型來建立的梧田,Java線程內(nèi)存模型是標準化的,屏蔽掉了低層不同計算機的區(qū)別侧蘸。
假設主內(nèi)存中有共享變量 int a=6裁眯,線程1把它改為7,線程2是未必能看到最新值的讳癌,因為線程和主內(nèi)存之間還有一個工作內(nèi)存穿稳,存儲這共享變量副本,線程1會先把工作內(nèi)存的值改為7析桥,在刷到主內(nèi)存中司草,但線程2的工作內(nèi)存值還是6,未必感知得到
證明:

public class VolatileVisibilityTest {

    private static boolean initFlag = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.println("waiting data...");
            while(!initFlag){

            }
            System.out.println("===============success");
        }).start();

        Thread.sleep(2000);

        new Thread(() -> prepareData()).start();
    }

    public static void prepareData(){
        System.out.println("prepare data...");
        initFlag = true;
        System.out.println("prepare data end...");
    }

}

在initFlag變?yōu)榱藅rue之后泡仗,但線程1還沒感知得到埋虹,仍處于死循環(huán)中

(3)volatile
只要使用了volatile修飾,就能馬上知道最新值娩怎,退出while循環(huán)
private static volatile boolean initFlag = false;

(4)volatile怎么保證線程可見性
內(nèi)存模型原子操作:
read(讀壬巍):從主內(nèi)存讀取數(shù)據(jù)
load(載入):將主內(nèi)存讀取到的數(shù)據(jù)寫入工作內(nèi)存
use(使用):從工作內(nèi)存讀取數(shù)據(jù)來計算
assign(賦值):將計算好的值重新賦值到工作內(nèi)存中
store(存儲):將工作內(nèi)存數(shù)據(jù)寫入主內(nèi)存
write(寫入):將store過去的變量值賦值給主內(nèi)存中的變量
lock(鎖定):將主內(nèi)存變量加鎖,標識為線程獨占狀態(tài)
unlock(解鎖):將主內(nèi)存變量解鎖截亦,解鎖后其他線程可以鎖定該變量


沒有volatile的流程

下面先慢慢展開

(5)緩存一致性協(xié)議(MESI)
多個cpu從主內(nèi)存讀取同一個數(shù)據(jù)到各自的高速緩存爬泥,當其中某個cpu修改了緩存里的數(shù)據(jù),該數(shù)據(jù)會馬上同步回主內(nèi)存崩瓤,其他cpu通過總線嗅探機制可以感知到數(shù)據(jù)的變化從而將自己緩存里的數(shù)據(jù)失效

(6)查看volatile匯編指令
首先下載hsdis-amd64.dll袍啡,然后復制到如C:\Program Files\Java\jdk1.8.0_261\jre\bin中,然后在運行java程序的時候却桶,要先配置一些jvm參數(shù)
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*VolatileVisibilityTest.prepareData 如果不同類名方法名的還得改改這里的內(nèi)容


如果提示了PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output境输,說明是PrintAssembly功能以及開啟,但系統(tǒng)不支持颖系,把hsdis-amd64.dll復制到如C:\Program Files\Java\jdk1.8.0_261\jre\bin\server目錄吧嗅剖,親測可行,以下就是輸出的匯編信息


其中*putstatic initFlag是JVM指令碼嘁扼,作用是給靜態(tài)變量賦值信粮。lock、mov這些都是匯編語言趁啸。
沒有volatile修飾的時候强缘,沒有l(wèi)ock指令督惰,volatile修飾的變量在賦值的時候會有l(wèi)ock。

(7)volatile緩存可見性實現(xiàn)原理
底層實現(xiàn)主要是通過匯編lock前綴指令欺旧,它會鎖定這塊內(nèi)存區(qū)域的緩存(緩存行鎖定)并回寫到主內(nèi)存
IA-32和Intel 64架構(gòu)軟件開發(fā)者手冊對lock指令的解析:
Ⅰ姑丑、會將當前處理器緩存行的數(shù)據(jù)立即寫回到系統(tǒng)內(nèi)存
Ⅱ、這個寫回內(nèi)存的操作會引起在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效(MESI協(xié)議)
Ⅲ辞友、提供內(nèi)存屏障功能,使lock前后指令不能重排序

其中第一點中震肮,lock能使變量賦值完后立即執(zhí)行store和write称龙,沒有l(wèi)ock的話是不會立即執(zhí)行的,會不知道什么時候執(zhí)行戳晌。所有l(wèi)ock會二話不說把數(shù)據(jù)刷回主內(nèi)存中鲫尊。立即刷回到主內(nèi)存的目的是讓多線程能及時感知到修改,強調(diào)及時性沦偎。
第二點疫向,MESI,指定的是
M狀態(tài)(修改)
E狀態(tài)(獨享)
S狀態(tài)(共享)
I狀態(tài)(無效)
把變量改為Invalid狀態(tài)豪嚎,數(shù)據(jù)就無效了搔驼。MESI內(nèi)容比較多,可以自己搜索一下侈询。
第三點后面再說舌涨。

假如面試提到volatile的原理,大概吹一下是通過匯編lock前綴指令扔字、然后這個指令的行為(立即囊嘉、MESI)就差不多了

(8)指令重排序與內(nèi)存屏障

  • 并發(fā)編程三大特寫:可見性、有序性革为、原子性

  • volatile保證可見性與有序性扭粱,但是不保證原子性,保證原子性需要借助synchronized這樣的鎖機制

  • 指令重排序:在不影響單線程程序執(zhí)行的結(jié)果的前提下震檩,計算機為了最大限度的發(fā)揮機器性能琢蛤,會對機器指令重排序優(yōu)化


  • 重排序會遵循as-if-serial與happens-before原則

  • 阿里面試題:雙重檢測鎖DCL對象半初始化問題

(9)什么是有序性
程序在執(zhí)行的時候是有順序行的,下面看一個例子恳蹲,猜一下可能會輸出什么

public class VolatileSerialTest {

    static int x = 0, y = 0;
    static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        Set<String> resultSet = new HashSet<>();

        for (int i = 0; i < 10000000; i++) {
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            Thread one = new Thread(() -> {
                a = y;
                x = 1;
            });

            Thread other = new Thread(() -> {
                b = x;
                y = 1;
            });

            one.start();
            other.start();
            one.join();
            other.join();

            resultSet.add("a=" + a + ",b=" + b);
            System.out.println(resultSet);
        }
    }

}

就這么一看虐块,ab的組合可能有00、01嘉蕾、10贺奠,但不會有11。
但實際上错忱,隨著程序的運行儡率,是會出現(xiàn)11的情況挂据,也就是a=1,也就是y=1儿普,也就是要先other線程先執(zhí)行完成崎逃,這樣b應該是0才對,但為什么會是1眉孩,這個就是指令重排序的影響瞒斩。
在最終指令執(zhí)行之前,可能會出現(xiàn)幾種情況的重排序共苛,如編譯器優(yōu)化重排序雌贱、指令級并行重排序、內(nèi)存系統(tǒng)重排序死遭,所以出現(xiàn)11的情況可能是執(zhí)行了
x=1; => y=1; => a=y; => b=x;
為什么會自作主張重排序广恢,大概是cpu認為當前指令比較耗時,而后面的指令結(jié)果會在其他地方使用呀潭,就先執(zhí)行了钉迷,讓其他地方可以不用等這么久,然后再執(zhí)行那個耗時的操作钠署。就是編譯器和處理器為了提高并行度糠聪。
這就出bug了,但不是計算機的bug踏幻,是你代碼的bug枷颊,是你自己沒考慮到11的情況,而不是理所當然的不可能有11该面,這就是并發(fā)的難點夭苗。

(10)重排序原則
操作系統(tǒng)不會亂排序,而是有依據(jù)的隔缀,會遵循as-if-serial與happens-before原則题造。
as-if-serial:不管怎么重排序,(單線程)程序的執(zhí)行結(jié)果不能被改變猾瘸。
假如有a=y和x=a界赔,這個就是不能重排序的,因為有依賴關系牵触,重排序的話x的值是變化的
像之前的程序淮悼,a=y和x=1,并沒有任何關系揽思,重排序并不影響結(jié)果袜腥,所以是允許重排序的,它不管是不是影響了其他線程钉汗,as-if-serial只管單線程

假設oracle的JDK產(chǎn)品經(jīng)理要求“a=y和x=a”的時候不能重排序羹令,“a=y和x=1”的時候可以重排序鲤屡,那么開發(fā)人員如何實現(xiàn)JDK這個軟件?
編譯原理會進行語義分析生成語義樹福侈,判斷代碼之間有沒有依賴關系酒来,有依賴就不能重排序

happens-before:
①程序順序原則:即在一個線程內(nèi)必須保證語義串行性,也就是說按照代碼順序執(zhí)行肪凛。
②鎖規(guī)則:解鎖(unlock)操作必然發(fā)生在后續(xù)的同一個鎖的加鎖(lock)之前堰汉,也就是說,如果對于一個鎖解鎖后伟墙,再加鎖衡奥,那么加鎖的動作必須在解鎖動作之后(同一個鎖)。

obj.lock();
obj.unlock();
obj.lock();
obj.unlock();
就是第2第3行不能重排序

③volatile規(guī)則:volatile變量的寫远荠,先發(fā)生于讀,這保證了volatile變量的可見性失息,簡單的理解就是譬淳,volatile變量在每次被線程訪問時,都強迫從主內(nèi)存中讀該變量的值盹兢,而當該變量發(fā)生變化時邻梆,又會強迫將最新的值刷新到主內(nèi)存,任何時刻绎秒,不同的線程總是能夠看到該變量的最新值浦妄。
④線程啟動規(guī)則:線程的start()方法先于它的每一個動作,即如果線程A在執(zhí)行線程B的start方法之前修改了共享變量的值见芹,那么當線程B執(zhí)行start方法時剂娄,線程A對共享變量的修改對線程B可見。
⑤傳遞性:A先于B玄呛,B先于C阅懦,那么A必然先于C
⑥線程終止規(guī)則:線程的所有操作先于線程的終結(jié),Thread.join()方法的作用是等待當前執(zhí)行的線程終止徘铝。假設在線程B終止之前耳胎,修改了共享變量,線程A從線程B的join方法成功放回后惕它,線程B對共享變量的修改將對線程A可見怕午。
⑦線程終端規(guī)則:對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測線程是否中斷淹魄。
⑧對象終結(jié)規(guī)則:對象的構(gòu)造函數(shù)執(zhí)行郁惜,結(jié)束先于finalize()方法。

(11)阿里面試題:雙重檢測鎖DCL對象半初始化問題
在阿里巴巴手冊中揭北,有一個推薦做法



早期的時候扳炬,雙重檢測鎖單例模式就是這么寫的吏颖,主要是解決并發(fā)的問題,避免重復初始化

public class DoubleCheckLockSingleton {

    private static DoubleCheckLockSingleton instance = null;

    private DoubleCheckLockSingleton(){}

    //雙重檢測鎖單例
    public static DoubleCheckLockSingleton getInstance(){
        if(instance == null){       //一重檢測
            synchronized (DoubleCheckLockSingleton.class){      //鎖
                if(instance == null){       //二重檢測
                    instance = new DoubleCheckLockSingleton();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        DoubleCheckLockSingleton instance = DoubleCheckLockSingleton.getInstance();
    }

}

但它存在點問題恨樟,我們先在idea中安裝jclasslib插件半醉,看看這個類的指令碼




其中synchronized對應的就是monitorenter至monitorexit


10 monitorenter
    獲取靜態(tài)變量
11 getstatic #2 <tuling/DoubleCheckLockSingleton.instance>
    判斷是不是為null
14 ifnonnull 27 (+13)
    是的話就new
17 new #3 <tuling/DoubleCheckLockSingleton>
20 dup
    執(zhí)行init方法(下面簡單說明一下,和這部分內(nèi)容無關)
21 invokespecial #4 <tuling/DoubleCheckLockSingleton.<init>>
    給變量賦值劝术,即instance=xx
24 putstatic #2 <tuling/DoubleCheckLockSingleton.instance>
27 aload_0
28 monitorexit

對象創(chuàng)建的主要流程



執(zhí)行<init>方法缩多,即對象按照程序員的意愿進行初始化。對應到語言層面上講养晋,就是為屬性賦值(主要衬吆,這與上面的賦零值不同,這是由程序員賦的值)绳泉,和執(zhí)行構(gòu)造方法逊抡。
回到問題上,究竟是會出現(xiàn)什么問題零酪?就是 invokespecial 和 putstatic 有可能重排序冒嫡,因為沒有違背as-if-serial與happens-before原則。
對于單線程下四苇,重排序的結(jié)果并沒有影響


個人理解

重排序之后:先把17行的地址給24行孝凌,instance已經(jīng)不為空了,但初始化是還沒完成的月腋,未執(zhí)行完init都是未完成蟀架,這時是對象的半初始化。當其他線程拿到了這個還沒完成初始化的變量時榆骚,就會出現(xiàn)問題片拍。(這種問題只有極端情況才會偶爾出現(xiàn))
所以要加volatile,就不會有重排序寨躁。

(12)內(nèi)存屏障
volatile底層會幫我們實現(xiàn)內(nèi)存屏障穆碎。如何理解內(nèi)存屏障?
如a=y; x=1; 如果不想這兩行代碼重排序职恳,在它們中間加一行標記性代碼所禀,跟重排序(CPU )做好約定,當遇到這個標記的時候就不能對它前后的代碼進行重排序放钦,那么這個標記性代碼就叫做內(nèi)存屏障

  • JVM規(guī)范定義的內(nèi)存屏障
屏障類型 指令示例 說明
LoadLoad Load1; LoadLoad; Load2 保證load1的讀取操作在load2及后續(xù)讀取操作之前執(zhí)行
StoreStore Store; StoreStore; Store2 在store2及其后的寫操作執(zhí)行前色徘,保證store1的寫操作已刷新到主內(nèi)存
LoadStore Load1; LoadStore; Store2 在Store2及其后的寫操作執(zhí)行前,保證load1的讀操作已讀取結(jié)束
StoreLoad Store1; StoreLoad; Load2 保證store1的寫操作已刷新到主內(nèi)存之后操禀,load2及其后的讀操作才能執(zhí)行

這些屏障是jdk規(guī)定的褂策,就是jdk的程序員實現(xiàn)的,load就是加載,store就是寫
LoadLoad:如b=a; c=a; 對應a來說斤寂,都是加載耿焊,只有它們之間加了LoadLoad,就不允許重排序遍搞。

  • JVM規(guī)定volatile需要實現(xiàn)的內(nèi)存屏障
a=2;  //volatile寫罗侯,a為volatile變量
StoreStore屏障
a=1;  //volatile寫
StoreLoad屏障
b=a;  //volatile讀
LoadLoad屏障
LoadStore屏障

也就是jdk的程序員在實現(xiàn)volatile的時候,就是在對它的操作前后都要加屏障溪猿,具體實現(xiàn)的話看看源碼 openjdk - https://github.com/openjdk/jdk/blob/master/src/hotspot/share/interpreter/zero/bytecodeInterpreter.cpp


這個switch就是對不同類型的volatile進行各自的操作钩杰,最后都會執(zhí)行一個 OrderAccess::storeload();,可以看看這個鏈接orderAccess_linux_x86.hpp

前面asm就是調(diào)用匯編語言的意思诊县,后面的就是匯編語言讲弄,還記得這個匯編語言嗎?回看上方第6點提到的沒有volatile修飾的時候依痊,沒有l(wèi)ock指令避除,volatile修飾的變量在賦值的時候會有l(wèi)ock。
lock前綴指令:在上方第7點中也提到提供內(nèi)存屏障功能胸嘁,使lock前后指令不能重排序驹饺,當很多的硬件看到這個lock前綴指令,就不會對前后左右的代碼進行重排序缴渊,就是一個約定好的代碼指令,看到就不能重排序鱼炒。

  • 不同CPU硬件對于JVM的內(nèi)存屏障規(guī)范實現(xiàn)指令不一樣
  • Intel CPU硬件級內(nèi)存屏障實現(xiàn)指令
    • lfence:是一種Load Barrier讀屏障衔沼,實現(xiàn)LoadLoad屏障
    • sfence:是一種Store Barrier寫屏障,實現(xiàn)StoreStore屏障
    • mfence: 是一種全能型的屏障昔瞧,具備lfence和sfence的能力指蚁,具有所有屏障能力
  • JVM低層簡化了內(nèi)存屏障硬件指令的實現(xiàn)
    • lock前綴:lock指令不是一種內(nèi)存屏障,但是它能完成類似內(nèi)存屏障的功能

(13)從Spring Cloud微服務框架源碼看下并發(fā)編程的應用
從github下載nacos源碼自晰,后續(xù)再看

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末凝化,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子酬荞,更是在濱河造成了極大的恐慌搓劫,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件混巧,死亡現(xiàn)場離奇詭異枪向,居然都是意外死亡,警方通過查閱死者的電腦和手機咧党,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進店門秘蛔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事深员「喝洌” “怎么了?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵倦畅,是天一觀的道長遮糖。 經(jīng)常有香客問我,道長滔迈,這世上最難降的妖魔是什么止吁? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮燎悍,結(jié)果婚禮上敬惦,老公的妹妹穿的比我還像新娘。我一直安慰自己谈山,他們只是感情好俄删,可當我...
    茶點故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著奏路,像睡著了一般畴椰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鸽粉,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天斜脂,我揣著相機與錄音,去河邊找鬼触机。 笑死帚戳,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的儡首。 我是一名探鬼主播片任,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蔬胯!你這毒婦竟也來了对供?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤氛濒,失蹤者是張志新(化名)和其女友劉穎产场,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體舞竿,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡涝动,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了炬灭。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片醋粟。...
    茶點故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡靡菇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出米愿,到底是詐尸還是另有隱情厦凤,我是刑警寧澤,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布育苟,位于F島的核電站较鼓,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏违柏。R本人自食惡果不足惜博烂,卻給世界環(huán)境...
    茶點故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望漱竖。 院中可真熱鬧禽篱,春花似錦、人聲如沸馍惹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽万矾。三九已至悼吱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間良狈,已是汗流浹背后添。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留薪丁,地道東北人吕朵。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像窥突,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子硫嘶,可洞房花燭夜當晚...
    茶點故事閱讀 45,107評論 2 356

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