Java內(nèi)存模型

在上篇《單例,真了解嗎?》中,提及到了重排序、volatile等概念,有的同學(xué)還沒接觸過這些,那么此篇就簡單介紹一下它們。

JMM

java中,所有實例域、靜態(tài)域、數(shù)組元素都存儲在堆中,堆可被多個線程共享。在多線程環(huán)境下,線程之間是如何通信和實現(xiàn)這些共享數(shù)據(jù)的同步?java的并發(fā)采用的是共享內(nèi)存模型。
JMM(Java Memory Model),Java內(nèi)存模型,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。JMM定義了線程和主內(nèi)存之間的抽象關(guān)系,多個線程的共享數(shù)據(jù)存在主內(nèi)存中,但是每個線程擁有自己的本地內(nèi)存,本地內(nèi)存中存儲的是該線程從主內(nèi)存那里讀取共享變量的副本(注意:本地內(nèi)存是個抽象概念,不存在的),下圖是JMM的抽象結(jié)構(gòu)示意圖:

JMM抽象結(jié)構(gòu)示意圖

從圖來看,如果線程A要和線程B通信的話,需要兩步:
①線程A把本地內(nèi)存A中的更新過的共享變量刷新到主內(nèi)存中;
②線程B再從主內(nèi)存中讀取線程A之前已更新過的共享變量。

happens-before

在JMM中,如果一個操作執(zhí)行的結(jié)果需要對另一個操作可見,那么這兩個操作之間就必須存在happens-before關(guān)系,當(dāng)然兩個操作可能屬于同一線程,也有可能屬于不同的線程。

happens-before規(guī)則:
1、程序次序規(guī)則:在一個單獨的線程中,按照程序代碼的執(zhí)行流順序,(時間上)先執(zhí)行的操作happens-before(時間上)后執(zhí)行的操作。
2、管理鎖定規(guī)則:一個unlock操作happens-before后面(時間上的先后順序,下同)對同一個鎖的lock操作。
3、volatile變量規(guī)則:對一個volatile變量的寫操作happens-before后面對該變量的讀操作。
4、傳遞性:如果操作A happens-before操作B,操作B happens-before操作C,那么可以得出A happens-before操作C。

注意:兩個操作之間具有happens-before關(guān)系,并不意味前一個操作必須要在后一個操作之前執(zhí)行!僅僅要求前一個操作的執(zhí)行結(jié)果,對于后一個操作是可見的,且前一個操作按順序排在后一個操作之前。

重排序

重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進行重新排序的一種方式。

數(shù)據(jù)依賴性

如果兩個操作訪問同一個變量,一個操作為寫操作,另一個是讀操作,那這兩個操作就存在數(shù)據(jù)依賴性。

//寫a
int a = 1;
//讀a
int b = a;

編譯器和處理器是不會對存在數(shù)據(jù)依賴關(guān)系的兩個操作做重排序的。(這里的數(shù)據(jù)依賴性僅針對單個處理器單線程中的操作,不同處理器不同線程間的數(shù)據(jù)依賴性不被編譯器和處理器考慮)

as-if-serial

as-if-serial的意思是,不管怎么重排序,單線程程序的執(zhí)行結(jié)果不能被改變。編譯器和處理器都必須遵守as-if-serial語義。

//寫a
int a = 1;  //A
//寫b
int b = 1;  //B
//寫c
int c = a + b;  //C

比如上面的這個例子,A和B之間沒有數(shù)據(jù)依賴性,A和C、B和C存在數(shù)據(jù)依賴關(guān)系,所以C不允許重排序到A和B前面(一旦排到A或B前面,執(zhí)行結(jié)果必定發(fā)生改變),A和B之間愛怎么排怎么排,對于執(zhí)行結(jié)果并無影響。

重排序?qū)Χ嗑€程的影響
public class Test8 {
    int a = 0;
    boolean flag = false;
    
    public void writer(){
        a = 1; //A
        flag = true; //B
    }
    
    public void reader(){
        if(flag){  //C
            int i = a * a; //D
        }
    }
}

假如有兩個線程E和F,E先執(zhí)行writer()方法,然后F執(zhí)行reader()方法。最終線程F執(zhí)行完會得到始終一直的i嗎?是不行的。因為操作A和操作B沒有數(shù)據(jù)依賴性,A和B可能發(fā)生重排序,操作C和操作D也沒有數(shù)據(jù)依賴性,C和D也可能發(fā)生重排序。假設(shè)操作A和B發(fā)生了重排序,會怎樣?
程序時序圖如下:

程序執(zhí)行時序圖

當(dāng)操作A和操作B發(fā)生重排序,線程E首先標(biāo)記變量flag,隨后線程F讀flag,flag為真再讀取a。此時,變量a并未被線程E寫入,多線程語義就被重排序破壞掉了。

內(nèi)存屏障

內(nèi)存屏障,又稱內(nèi)存柵欄,是一個CPU指令,基本上它是一條這樣的指令:
1、保證特定操作的執(zhí)行順序。
2、影響某些數(shù)據(jù)(或則是某條指令的執(zhí)行結(jié)果)的內(nèi)存可見性。
編譯器和CPU能夠重排序指令,保證最終相同的結(jié)果,嘗試優(yōu)化性能。插入一條Memory Barrier會告訴編譯器和CPU:不管什么指令都不能和這條Memory Barrier指令重排序。
Memory Barrier所做的另外一件事是強制刷出各種CPU cache,如一個 Write-Barrier(寫入屏障)將刷出所有在 Barrier 之前寫入 cache 的數(shù)據(jù),因此,任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本。

volatile

volatile是基于Memory Barrier實現(xiàn)的,可以將volatile簡單看作是一個輕量級鎖。

public class Test8 {
    //使用volatile修飾vl
    volatile  long vl = 0L;

    public long getVl() {
        return vl;
    }

    public void setVl(long vl) {
        this.vl = vl;
    }

    public void getAndIncrement(){
        vl++;
    }
}

假如有多個線程分別調(diào)用上面程序的三個方法,程序在語義上和下面程序等價。

public class Test8 {
    long vl = 0L;

    public synchronized long getVl() {
        return vl;
    }

    public synchronized void setVl(long vl) {
        this.vl = vl;
    }

    public void getAndIncrement(){
        long temp = getVl();
        temp += 1L;
        setVl(temp);
    }
}

一個volatile變量的單個讀/寫操作,與一個普通變量的讀/寫操作都使用同一個鎖來同步效果一樣。鎖的happens-before規(guī)則保證釋放鎖和獲取鎖的兩個線程間的內(nèi)存可見性,所以對一個volatile變量的讀,總能看到任意線程對這個volatile變量最后的寫入。

簡單來說,volatile擁有以下特性;
可見性,對一個volatile變量的讀,總能看到任意線程對這個volatile變量最后的寫入。
不保證原子性,對任意單個volatile變量的讀/寫具有原子性,但對于像volatile++這種復(fù)合操作不具有原子性。

如果一個變量是volatile修飾的,JMM會在寫入這個字段之后插進一個Write-Barrier指令,并在讀這個字段之前插入一個Read-Barrier指令。
當(dāng)寫一個volatile變量時,JMM會把該線程對應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存中。
當(dāng)讀一個volatile變量時,JMM會把該線程對應(yīng)的本地內(nèi)存置為無效,然后再從主內(nèi)存中讀取共享變量。

問題:volatile為什么對復(fù)合操作不保證原子性?
答:拿上面的案例來講,某個時刻變量vl為10,線程1要調(diào)用getAndIncrement(),線程1先讀取vl的原始值,然后線程1被阻塞了;然后線程2也要調(diào)用getAndIncrement(),線程2也會先讀取vl的原始值,由于線程1直對vl進行了讀操作,并未做修改,所以不會線程2本地內(nèi)存中的緩存變量vl失效,不會導(dǎo)致主內(nèi)存的vl刷新,所以線程2讀取的vl還是10,調(diào)用完getAndIncrement()后,把11寫入本地內(nèi)存,再寫入主內(nèi)存。接著,線程1也調(diào)用了getAndIncrement(),由于已經(jīng)讀取了vl,線程1本地內(nèi)存的vl依然為10,自增完后vl變?yōu)閷ο?1,然后將11寫入本地內(nèi)存,最后寫入主內(nèi)存。2個線程一共執(zhí)行了兩次自增操作,但是最后vl是11不是12。

問題:對于volatile不能保證原子性,如何解決?
答:用synchronized或者Lock來加鎖。

問題:volatile和synchronized的區(qū)別有哪些?
答:①volatile只能修飾變量,而synchronized可修飾變量、方法、代碼塊;②volatile在多線程中不存在阻塞問題,synchronized存在阻塞問題;③volatile能保證數(shù)據(jù)的可見性,不能保證數(shù)據(jù)的原子性,synchronized能保證可見性、原子性;④volatile解決的是變量在線程間的可見性,而synchronized解決的是線程之間訪問資源的同步性。

總結(jié)

此篇主要介紹了JMM、happens-before、重排序、內(nèi)存屏障、volatile的一些基本概念及重要性質(zhì)。只有真正理解線程之間的通信機制,才能寫出健壯的并發(fā)程序。
本篇頂多算個基礎(chǔ)介紹,需要深入了解的同學(xué)可以閱讀書籍《并發(fā)編程的藝術(shù)》、《并發(fā)編程實戰(zhàn)》。

略陳固陋,如有不當(dāng)之處,歡迎各位看官批評指正!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容