深入理解JMM+Volatile

計算機多核并發(fā)緩存架構(gòu)

下圖是計算機運行架構(gòu)圖:


計算機運行緩存并發(fā)架構(gòu)圖.png

??由于cpu的運行程序速度遠大于主存儲的速度裳食,所以會在主存RAM和CPU之間加多級高速緩存矛市,緩存的速度接近cpu的運行速度,木桶效應诲祸,水能裝多少浊吏,取決于短板而昨,因此這樣會大大提高計算機的運行速度。

java內(nèi)存模型

定義(摘抄《深入理解+Java+內(nèi)存模型_程曉明》)

??java線程內(nèi)存模型跟cpu緩存模型類似找田,是基于cpu緩存模型來建立的歌憨,java線程內(nèi)存模型是標準化的。
在java中墩衙,所有實例域务嫡,靜態(tài)域和數(shù)組元素存儲在堆內(nèi)存中,堆內(nèi)存在線程之間共享漆改。

??局部變量 心铃,方法定義參數(shù)和異常處理器參數(shù),這些不會在線程之間 共享籽懦,它們不會有內(nèi)存可見性問題于个,也不受內(nèi)存模型的影響

??Java線程之間的通信由Java內(nèi)存模型(本文簡稱為JMM)控制,JMM決定一個 線程對共享變量的寫入何時對另一個線程可見暮顺。從抽象的角度來看厅篓,JMM定義了 線程和主內(nèi)存之間的抽象關系:線程之間的共享變量存儲在主內(nèi)存(main memory)中,每個線程都有一個私有的本地內(nèi)存(local memory)捶码,本地內(nèi)存 中存儲了該線程以讀/寫共享變量的副本羽氮。本地內(nèi)存是JMM的一個抽象概念,并不 真實存在惫恼。它涵蓋了緩存档押,寫緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化祈纯。Java 內(nèi)存模型的抽象示意圖如下:

以上還是比較通俗易懂的

內(nèi)存模型的三大特性

可見性:
  可見性是一種復雜的屬性令宿,因為可見性中的錯誤總是會違背我們的直覺。通常腕窥,我們無法確保執(zhí)行讀操作的線程能適時地看到其他線程寫入的值粒没,有時甚至是根本不可能的事情。為了確保多個線程之間對內(nèi)存寫入操作的可見性簇爆,必須使用同步機制癞松。
  可見性,是指線程之間的可見性入蛆,一個線程修改的狀態(tài)對另一個線程是可見的响蓉。也就是一個線程修改的結(jié)果。另一個線程馬上就能看到哨毁。比如:用volatile修飾的變量枫甲,就會具有可見性。volatile修飾的變量不允許線程內(nèi)部緩存和重排序,即直接修改內(nèi)存言秸。所以對其他線程是可見的软能。但是這里需要注意一個問題蛔溃,volatile只能讓被他修飾內(nèi)容具有可見性堵腹,但不能保證它具有原子性和簸。比如 volatile int a = 0;之后有一個操作 a++抄沮;這個變量a具有可見性,但是a++ 依然是一個非原子操作岖瑰,也就是這個操作同樣存在線程安全問題叛买。
  在 Java 中 volatile、synchronized 和 final 實現(xiàn)可見性蹋订。

原子性:
  原子是世界上的最小單位率挣,具有不可分割性。比如 a=0露戒;(a非long和double類型) 這個操作是不可分割的椒功,那么我們說這個操作時原子操作。再比如:a++智什; 這個操作實際是a = a + 1动漾;是可分割的,所以他不是一個原子操作荠锭。非原子操作都會存在線程安全問題旱眯,需要我們使用同步技術(sychronized)來讓它變成一個原子操作。一個操作是原子操作证九,那么我們稱它具有原子性删豺。java的concurrent包下提供了一些原子類,我們可以通過閱讀API來了解這些原子類的用法愧怜。比如:AtomicInteger呀页、AtomicLong、AtomicReference等叫搁。
  在 Java 中 synchronized 和在 lock赔桌、unlock 中操作保證原子性。

有序性:
  Java 語言提供了 volatile 和 synchronized 兩個關鍵字來保證線程之間操作的有序性渴逻,volatile 是因為其本身包含“禁止指令重排序”的語義疾党,synchronized 是由“一個變量在同一個時刻只允許一條線程對其進行 lock 操作”這條規(guī)則獲得的,此規(guī)則決定了持有同一個對象鎖的兩個同步塊只能串行執(zhí)行惨奕。

volatile一致性

看一下下面代碼:

package com.zhaoyan.volatileexample;

public class VolatileVisibleness {

    private static boolean flag =false;
    public static void main(String[] args)  throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("等待數(shù)據(jù)準備中---");
                while(!flag){

                }
                System.out.println("數(shù)據(jù)準備結(jié)束雪位,開始執(zhí)行...");
            }
        }).start();
        Thread.sleep(2000);
        new Thread(new Runnable() {
            @Override
            public void run() {
                setFlag();
            }
        }).start();
    }

    public static void setFlag() {
        System.out.println("數(shù)據(jù)準備中---");
        VolatileVisibleness.flag = true;
        System.out.println("數(shù)據(jù)準備結(jié)束---");
    }

}

以上代碼分析:
??主函數(shù)中有兩個線程,一個線程用來獲取邏輯處理標簽flag結(jié)果標簽梨撞,并繼續(xù)執(zhí)行程序雹洗,一個線程用來處理flag標簽邏輯香罐。我們想要的結(jié)果是,在線程休眠2秒之后时肿,flag=true的時候庇茫,被第一個線程獲取到,并繼續(xù)執(zhí)行 程序螃成,打印數(shù)據(jù)準備結(jié)束旦签,開始執(zhí)行...

執(zhí)行結(jié)果如下:


如圖可知,程序在死循環(huán)等待中寸宏,也就是說
image.png
這行代碼執(zhí)行之后宁炫,while的條件沒有被執(zhí)行,數(shù)據(jù)準備結(jié)束氮凝,開始執(zhí)行...未被打痈岢病;

這個現(xiàn)象驗證了內(nèi)存模型中存在這樣的一塊區(qū)域是線程獨享的罩阵。


??當我修改線程B中的局部變量后竿秆,線程A并不知道flag的值已經(jīng)被修改,導致程序死循環(huán)永脓。
如何達到預期的效果袍辞,使數(shù)據(jù)準備結(jié)束,開始執(zhí)行...開始執(zhí)行呢常摧!就是加volatile關鍵字修改變量搅吁。

private static volatile boolean flag =false;

然后在執(zhí)行的結(jié)果如下:

等待數(shù)據(jù)準備中---
數(shù)據(jù)準備中---
數(shù)據(jù)準備結(jié)束---
數(shù)據(jù)準備結(jié)束,開始執(zhí)行...

內(nèi)存之間的原子操作

Java 內(nèi)存模型對主內(nèi)存與工作內(nèi)存之間的具體交互協(xié)議定義了八種操作落午,具體如下:

read(讀然雅场):作用于主內(nèi)存變量,把一個變量從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中溃斋,以便隨后的 load 動作使用界拦。

load(載入):作用于工作內(nèi)存變量,把 read 操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中梗劫。

use(使用):作用于工作內(nèi)存變量享甸,把工作內(nèi)存中的一個變量值傳遞給執(zhí)行引擎,每當虛擬機遇到一個需要使用變量值的字節(jié)碼指令時執(zhí)行此操作梳侨。

assign(賦值):作用于工作內(nèi)存變量蛉威,把一個從執(zhí)行引擎接收的值賦值給工作內(nèi)存的變量,每當虛擬機遇到一個需要給變量進行賦值的字節(jié)碼指令時執(zhí)行此操作走哺。

store(存儲):作用于工作內(nèi)存變量蚯嫌,把工作內(nèi)存中一個變量的值傳遞到主內(nèi)存中,以便后續(xù) write 操作。

write(寫入):作用于主內(nèi)存變量择示,把 store 操作從工作內(nèi)存中得到的值放入主內(nèi)存變量中束凑。

lock(鎖定):作用于主內(nèi)存變量,把一個變量標識為一條線程獨占狀態(tài)栅盲。

unlock(解鎖):作用于主內(nèi)存變量汪诉,把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定剪菱。

普通變量底層執(zhí)行流程

根據(jù)前面代碼摩瞎,我們根據(jù)這個圖來說明一下程序的執(zhí)行流程:


volatile修飾后的變量底層執(zhí)行流程

如何讓工作內(nèi)存的變量相等!P⒊!!


image.png

早期解決變量一致性的問題設計:

總線加鎖:

??cpu從主內(nèi)存讀取數(shù)據(jù)到高速緩存蚓哩,會在總線對這個數(shù)據(jù)加鎖构灸,這樣其他cpu沒法去讀或?qū)戇@個數(shù)據(jù),直到cpu使用完數(shù)據(jù)釋放鎖之后岸梨,其他cpu才能讀取該數(shù)據(jù)喜颁。



??在線程B讀數(shù)據(jù)到工作內(nèi)存中的時候進行加鎖,一直到cpu執(zhí)行結(jié)束并將數(shù)據(jù)重新寫到對內(nèi)存之后才能釋放鎖曹阔,這個時候線程A再讀取已經(jīng)改變的數(shù)據(jù)加載到線程A的本地內(nèi)存中操作半开。
??這樣的做存在的問題有:
(1)性能降低
(2)沒有充分利用多核計算機的特性

緩存一致性協(xié)議:

??多個cpu從主內(nèi)存讀取同一個數(shù)據(jù)到各自的高速緩存,當其中某個cpu修改了緩存里的數(shù)據(jù)赃份,該數(shù)據(jù)會馬上同步回主內(nèi)存寂拆,其他cpu通過總線嗅探機制(監(jiān)聽)可以感知數(shù)據(jù)的變化從而將自己的緩存里的數(shù)據(jù)失效。



??對比總線加鎖抓韩,緩存一致性協(xié)議的鎖的力度小了很多纠永,只在總線寫入前開始加鎖,為了保證總線監(jiān)聽到的數(shù)據(jù)一定是被寫入了堆內(nèi)存谒拴〕⒔看volatile的執(zhí)行匯編底層語言,也會看到在數(shù)據(jù)修改前會加lock英上。

??以上是volatile的可見性炭序。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile 變量最后的寫入苍日;

volatile原子性

??在JSR -133之前的舊內(nèi)存模型中惭聂,一個64位long/ double型變量的讀/ 寫操作可以被拆分為兩個32位的讀/寫操作來執(zhí)行。從JSR -133內(nèi)存模型開始 (即從JDK5開始)易遣,僅僅只允許把一個64位long/ double型變量的寫操作拆分 為兩個32位的寫操作來執(zhí)行彼妻,任意的讀操作在JSR -133中都必須具有原子性(即 任意讀操作必須要在單個讀事務中執(zhí)行)

??對任意單個volatile變量的讀/寫具有原子性,但類似于volatile++這 種復合操作不具有原子性。

??鎖的happens-before規(guī)則保證釋放鎖和獲取鎖的兩個線程之間的內(nèi)存可見性侨歉,這 意味著對一個volatile變量的讀屋摇,總是能看到(任意線程)對這個volatile變量最 后的寫入。

看一下下面代碼:

package com.zhaoyan.volatileexample;
public class VolatileAtomic {

    private static volatile long num = 0L;

    private static void increase() {
        num++;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            Thread thread = threads[i];
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        for (Thread thread : threads) {
            thread.join();
        }
        System.out.println(num);
    }
}

以上代碼分析如下:
??十個線程一起執(zhí)行num+1的操作幽邓,等十個線程都執(zhí)行結(jié)束之后炮温,將num的數(shù)值輸出,num用volatile修飾牵舵,保證內(nèi)存的可見性柒啤;

代碼執(zhí)行結(jié)果如下:



可以看到,執(zhí)行的結(jié)果是10000以下畸颅;

為什么會出現(xiàn)這種情況担巩?
結(jié)合圖例:

jieguo3

分析:
??線程A加載num之后,use没炒,進行num++涛癌,之后assign,num=1送火,在num store總線加鎖之前拳话,線程B執(zhí)行,num的assign种吸,num=1弃衍;這個時候,線程A進行l(wèi)ock坚俗,根據(jù)總線嗅探機制镜盯,這個時候線程B發(fā)現(xiàn)數(shù)據(jù)有變化,進行變量失效坦冠,導致形耗,線程B執(zhí)行的mum++的結(jié)果值失效,進而形成程序執(zhí)行的效果辙浑;
??鎖的語義決定了臨界區(qū)代碼的執(zhí)行具有原子性激涤。這意味著即使是64位的long型和 double型變量,只要它是volatile變量判呕,對該變量的讀寫就將具有原子性倦踢。如果 是多個volatile操作或類似于volatile++這種復合操作,這些操作整體上不具有原 子性侠草。
以上我們驗證了volatile的原子性辱挥;

volatile有序性

??在執(zhí)行程序時為了提高性能,編譯器和處理器常常會對指令做重排序边涕。
??為了實現(xiàn)volatile的內(nèi)存語義晤碘,編譯器在生成字節(jié)碼時褂微,會在指令序列中插入內(nèi)存 屏障來禁止特定類型的處理器重排序。
??代碼樣例:

package com.zhaoyan.volatileexample;

import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName VolatileOrder
 * @Description TODO
 * @Author zhaoyan
 * @Date 2019/7/31 11:03
 * @Version 1.0
 **/
public class VolatileOrder {
    static int x = 0, y = 0;


    public static void main(String[] args) throws Exception {
        Map<String, Integer> map = new HashMap<>();
        for (int i = 0; i < 10000; i++) {
            x = 0;
            y = 0;
            map.clear();
            Thread one = new Thread(new Runnable() {
                @Override
                public void run() {
                    int a = y; //1
                    x = 1;     //2
                    map.put("a", a);
                }
            });
            Thread two = new Thread(new Runnable() {
                @Override
                public void run() {
                    int b = x;   //3
                    y = 1;       //4
                    map.put("b", b);
                }
            });

            one.start();
            two.start();
            one.join();
            two.join();
            System.out.println(map.toString());
        }
    }
}

以上代碼分析:
正常理解的情況下园爷,我們能推測出程序答應的結(jié)果可能是
(1)線程1先執(zhí)行宠蚂,然后再執(zhí)行線程2,這樣輸出的結(jié)果是[0,1]童社;
(2)線程2先執(zhí)行求厕,然后再執(zhí)行線程1,這樣輸出的結(jié)果是[1,0]扰楼;
(3)線程1先執(zhí)行呀癣,但是沒有執(zhí)行到x=1的時候,線程2 執(zhí)行弦赖,結(jié)果是[0,0]项栏;

實際結(jié)果如下:


1.png

2.png

3.png

4.png

??我們看到執(zhí)行結(jié)果有4種情況,那么[1,1]的情況是怎么出現(xiàn)的呢蹬竖?
??這里面涉及到JMM模型的重排序原理忘嫉,主要有if-else-serial語義,數(shù)據(jù)依賴性案腺,程序順序規(guī)則等相關知識。不一一說明了康吵;
volatile修飾的變量可以阻止這種重排序劈榨,利用的內(nèi)存屏障的原理;
??volatile的內(nèi)存語義:嚴格限制編譯器和處理器對volatile變量與普通變量的重排 序晦嵌,確保volatile的寫-讀和鎖的釋放-獲取具有相同的內(nèi)存語義同辣。從編譯器重排序 規(guī)則和處理器內(nèi)存屏障插入策略來看,只要volatile變量與普通變量之間的重排序 可能會破壞volatile的內(nèi)存語義惭载,這種重排序就會被編譯器重排序規(guī)則和處理器內(nèi) 存屏障插入策略禁止旱函。
以上說明了volatile在并發(fā)三大特性中所發(fā)揮的作用,如果覺得可以描滔,請留下愛心棒妨;

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市含长,隨后出現(xiàn)的幾起案子券腔,更是在濱河造成了極大的恐慌,老刑警劉巖拘泞,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纷纫,死亡現(xiàn)場離奇詭異,居然都是意外死亡陪腌,警方通過查閱死者的電腦和手機辱魁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門烟瞧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人染簇,你說我怎么就攤上這事参滴。” “怎么了剖笙?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵卵洗,是天一觀的道長。 經(jīng)常有香客問我弥咪,道長过蹂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任聚至,我火速辦了婚禮酷勺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘扳躬。我一直安慰自己脆诉,他們只是感情好,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布贷币。 她就那樣靜靜地躺著击胜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪役纹。 梳的紋絲不亂的頭發(fā)上偶摔,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天,我揣著相機與錄音促脉,去河邊找鬼辰斋。 笑死,一個胖子當著我的面吹牛瘸味,可吹牛的內(nèi)容都是我干的宫仗。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼旁仿,長吁一口氣:“原來是場噩夢啊……” “哼藕夫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起丁逝,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤汁胆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后霜幼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嫩码,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年罪既,在試婚紗的時候發(fā)現(xiàn)自己被綠了铸题。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铡恕。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖丢间,靈堂內(nèi)的尸體忽然破棺而出探熔,到底是詐尸還是另有隱情,我是刑警寧澤烘挫,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布诀艰,位于F島的核電站,受9級特大地震影響饮六,放射性物質(zhì)發(fā)生泄漏其垄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一卤橄、第九天 我趴在偏房一處隱蔽的房頂上張望绿满。 院中可真熱鬧,春花似錦窟扑、人聲如沸喇颁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽橘霎。三九已至,卻和暖如春殖属,著一層夾襖步出監(jiān)牢的瞬間茎毁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工忱辅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谭溉。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓墙懂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親扮念。 傳聞我的和親對象是個殘疾皇子损搬,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

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