JMM內(nèi)存模型

什么是JMM內(nèi)存模型

內(nèi)存模型可以理解為在特定的操作協(xié)議下,對(duì)特定的內(nèi)存或高速緩存進(jìn)行讀寫訪問的過程抽象描述,不同架構(gòu)下的物理機(jī)擁有不一樣的內(nèi)存模型。

JMM(Java內(nèi)存模型)源于CPU架構(gòu)的內(nèi)存模型(用于解決多處理器架構(gòu)系統(tǒng)中的緩存一致性問題)。JVM為了屏蔽各個(gè)硬件平臺(tái)和操作系統(tǒng)對(duì)內(nèi)存訪問機(jī)制的差異化阔墩,提出了JMM概念。因此它不是對(duì)物理內(nèi)存的規(guī)范瓶珊,而是在虛擬機(jī)基礎(chǔ)上進(jìn)行的規(guī)范從而實(shí)現(xiàn)平臺(tái)一致性啸箫。

Java內(nèi)存模型(Java Memory Model,JMM)是一種抽象的概念伞芹,它描述的是一組規(guī)則或規(guī)范筐高,通過這組規(guī)范定義了程序中各個(gè)變量(包括實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素)的訪問方式丑瞧。

JMM結(jié)構(gòu)規(guī)范

JMM規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存(Main Memory)中。每個(gè)線程還有自己的工作內(nèi)存(Working Memory)蜀肘,線程的工作內(nèi)存中保存了該線程使用到的變量的主內(nèi)存的副本拷貝绊汹,線程對(duì)變量的所有操作(讀取、賦值等)都必須在工作內(nèi)存中進(jìn)行扮宠,而不能直接讀寫主內(nèi)存中的變量西乖。不同的線程之間也無法直接訪問對(duì)方工作內(nèi)存中的變量,線程之間值的傳遞都需要通過主內(nèi)存來完成

主內(nèi)存&工作內(nèi)存

主內(nèi)存

主要存儲(chǔ)的是Java實(shí)例對(duì)象坛增,所有線程創(chuàng)建的實(shí)例對(duì)象都存放在主內(nèi)存中(除開開啟了逃逸分析和標(biāo)量替換的棧上分配和TLAB分配)获雕,不管該實(shí)例對(duì)象是成員變量還是方法中的本地變量,也包括共享的類信息收捣、常量届案、靜態(tài)變量。由于是共享數(shù)據(jù)區(qū)域罢艾,多條線程對(duì)同一個(gè)變量進(jìn)行非原子性操作時(shí)可能會(huì)發(fā)生線程安全問題

工作內(nèi)存

主要存儲(chǔ)當(dāng)前方法的所有本地變量信息(工作內(nèi)存中存儲(chǔ)這主內(nèi)存中的變量副本拷貝)楣颠,每個(gè)線程只能訪問自己的工作內(nèi)存尽纽,即線程中的本地變量對(duì)其他線程是不可見的,就算是兩個(gè)線程執(zhí)行的是同一段代碼童漩,它們也會(huì)各自在自己的工作內(nèi)存中創(chuàng)建屬于當(dāng)前線程的本地變量弄贿,當(dāng)然也包括字節(jié)碼行號(hào)指示器、相關(guān)Native方法的信息

交互協(xié)議

八大原子操作
主內(nèi)存與工作內(nèi)存之間的具體交互協(xié)議矫膨,Java內(nèi)存模型定義了八種操作來完成:

  • Lock(鎖定)
    作用于主內(nèi)存的變量差凹,把一個(gè)變量標(biāo)記為一條線程獨(dú)占狀態(tài)
  • Read(讀取)
    作用于主內(nèi)存的變量侧馅,把一個(gè)變量從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中
  • Load(加載)
    作用于工作內(nèi)存的變量危尿,把Read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中
  • Use(使用)
    作用于工作內(nèi)存的變量,把工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎施禾,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作
  • Assign(賦值)
    作用于工作內(nèi)存的變量脚线,把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作
  • Store(存儲(chǔ))
    作用于工作內(nèi)存的變量弥搞,把工作內(nèi)存中的一個(gè)變量的值傳遞到主內(nèi)存中
  • Write(寫入)
    作用于主內(nèi)存的變量邮绿,把Store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中
  • Unlock(解鎖)
    作用于主內(nèi)存的變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來攀例,釋放后的變量才可以被其他線程鎖定


同步原則

  • 不允許一個(gè)線程無原因地(沒有發(fā)生過assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中
  • 一個(gè)新的變量只能在主內(nèi)存中誕生船逮,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量。即對(duì)一個(gè)變量實(shí)施use和store之前粤铭,必須先執(zhí)行assign和load操作
  • 如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作挖胃,將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量之前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值
  • 如果一個(gè)變量事先沒有被lock操作鎖定梆惯,則不允許對(duì)它執(zhí)行unlock操作酱鸭;也不允許去unlock一個(gè)被其他線程鎖定的變量
  • 對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)

與JVM內(nèi)存結(jié)構(gòu)的區(qū)別

JVM內(nèi)存結(jié)構(gòu)和JMM內(nèi)存模型是完全兩個(gè)不同的概念垛吗。

JVM內(nèi)存結(jié)構(gòu)是處于Java虛擬機(jī)層面的凹髓,是運(yùn)行時(shí)對(duì)Java進(jìn)程占用的內(nèi)存進(jìn)行的一種邏輯上的劃分,通過不同數(shù)據(jù)結(jié)構(gòu)來對(duì)申請(qǐng)的內(nèi)存進(jìn)行不同使用怯屉。對(duì)操作系統(tǒng)來說蔚舀,本質(zhì)上JVM還是存在于主存中

JMM是Java語言與OS和硬件架構(gòu)層面的,本質(zhì)上JMM并不能說是某種技術(shù)實(shí)現(xiàn)锨络,而是一種在多線程并發(fā)情況下對(duì)于共享變量讀寫的規(guī)范赌躺。JMM屏蔽了不同操作系統(tǒng)差異,是跨平臺(tái)可用的內(nèi)存模型羡儿,用來描述線程的數(shù)據(jù)在何時(shí)從主內(nèi)存讀取礼患,何時(shí)寫入主內(nèi)存,解決線程間數(shù)據(jù)共享和傳遞的問題

OS與JVM線程關(guān)系

Java線程的實(shí)現(xiàn)是基于一對(duì)一的線程模型。所謂一對(duì)一模型讶泰,實(shí)際上就是通過語言級(jí)別層面程序去間接調(diào)用系統(tǒng)內(nèi)核的線程模型咏瑟。

我們?cè)谑褂肑ava線程時(shí),如new Thread(Runnable);痪署,JVM內(nèi)部是調(diào)用當(dāng)前操作系統(tǒng)的內(nèi)核線程來完成當(dāng)前Runnable任務(wù)码泞。我們編寫的多線程程序?qū)儆谡Z言層面的,程序一般不會(huì)直接去調(diào)用內(nèi)核線程狼犯,而是創(chuàng)建一個(gè)應(yīng)用線程映射到一個(gè)內(nèi)核線程余寥,然后通過該線程調(diào)用內(nèi)核線程,進(jìn)而由操作系統(tǒng)內(nèi)核將任務(wù)映射到各個(gè)處理器悯森。這種應(yīng)用線程與內(nèi)核線程間一對(duì)一的關(guān)系就稱為Java程序中的線程與OS的一對(duì)一模型宋舷。

三大特性

JMM是圍繞著并發(fā)編程中原子性、可見性瓢姻、有序性這三個(gè)特征來建立的

原子性

原子性指的是一個(gè)操作是不可中斷的祝蝠,即使是在多線程環(huán)境下,一個(gè)操作一旦開始就不會(huì)被其他線程影響幻碱。

基本類型數(shù)據(jù)的訪問大都是原子操作绎狭,long 和 double 類型的變量是64位的,在32位JVM中褥傍,32位的JVM會(huì)將64位數(shù)據(jù)的讀寫操作分為2次32位的讀寫操作來進(jìn)行儡嘶,這就導(dǎo)致long、double類型的變量在32位虛擬機(jī)中是非原子性操作恍风,數(shù)據(jù)有可能會(huì)被破壞蹦狂,也就意味著多線程在并發(fā)訪問的時(shí)候是線程非安全的

可見性

一個(gè)線程對(duì)共享變量做了修改后,其他的線程立即能夠看到該變量的這種修改

對(duì)于串行程序來說朋贬,可見性是不存在的凯楔,因?yàn)槲覀冊(cè)谌魏我粋€(gè)操作中修改了某個(gè)變量的值,后續(xù)的操作中都能讀取這個(gè)變量值锦募,并且是修改過的新值摆屯。但在多線程環(huán)境中,工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象就會(huì)造成可見性問題御滩,另外重排序也可能導(dǎo)致可見性問題

有序性

對(duì)于一個(gè)線程的代碼而言,代碼的執(zhí)行總是從前往后依次執(zhí)行的

在單線程環(huán)境下党远,代碼由編碼的順序從上往下執(zhí)行削解,就算發(fā)生指令重排序,由于所有硬件優(yōu)化的前提都是必須遵守 as-if-serial 語義沟娱,所以不管怎么排序氛驮,都不會(huì)且不能影響單線程程序的執(zhí)行結(jié)果。對(duì)于多線程環(huán)境济似,則可能出現(xiàn)亂序現(xiàn)象矫废,因?yàn)槌绦蚓幾g成機(jī)器碼指令后可能會(huì)出現(xiàn)指令重排序現(xiàn)象盏缤,重排后的指令與原指令的順序未必一致(因?yàn)橹噶钪嘏判颥F(xiàn)象以及工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象導(dǎo)致)。

解決方案

對(duì)于原子性問題蓖扑,JVM提供了對(duì)基本數(shù)據(jù)類型讀寫操作的原子性唉铜,對(duì)于單個(gè)變量(包括64位long和double)可以用volatile關(guān)鍵字來保證讀寫操作的原子性,但volatile關(guān)鍵字對(duì)多個(gè)volatile操作或類似volatile++這種復(fù)合操作不具有原子性律杠;對(duì)于方法級(jí)別或代碼塊級(jí)別的原子性操作潭流,可以使用 synchronized 關(guān)鍵字或Lock鎖來保證程序執(zhí)行的原子性。
對(duì)于工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象導(dǎo)致的可見性問題柜去,可以使用加鎖或volatile關(guān)鍵字來解決
對(duì)于指令重排序?qū)е碌目梢娦詥栴}和有序性問題灰嫉,可以利用volatile關(guān)鍵字解決
同時(shí),JMM內(nèi)部還定義了一套happens-before原則來保證多線程環(huán)境下兩個(gè)操作間的原子性嗓奢、可見性以及有序性

as-if-serial語義

無論什么語言讼撒,只要操作之間沒有數(shù)據(jù)依賴性,編譯器和CPU都可以任意重排序股耽,因?yàn)閳?zhí)行結(jié)果不會(huì)改變根盒,代碼看起來就像是完全串行地一行行從頭執(zhí)行到尾,這就是as-if-serial語義

對(duì)于單線程來說豺谈,編譯器和CPU可能做了重排序郑象,但開發(fā)者感知不到,也不存在內(nèi)存可見性問題

對(duì)于多線程來說茬末,線程之間的數(shù)據(jù)依賴性太復(fù)雜厂榛,編譯器和CPU沒有辦法完全理解這種依賴性并據(jù)此做出最合理的優(yōu)化。編譯器和CPU只能保證每個(gè)線程的 as-if-serial 語義丽惭,線程之間的數(shù)據(jù)依賴和相互影響击奶,需要上層來確定。

happens-before原則

從JDK5開始责掏,Java使用新的JSR-133內(nèi)存模型柜砾。JSR-133提出了 happens-before 概念,通過這個(gè)概念來闡述操作之間的內(nèi)存可見性换衬。
happens-before 表達(dá)的是:前一個(gè)操作的結(jié)果需要對(duì)后續(xù)操作是可見的痰驱。這里提到的兩個(gè)操作既可以是在一個(gè)線程內(nèi),也可以是不同線程之間瞳浦。

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }

  public void reader() {
    if (v == true) {
      //x的值是多少呢担映?
    }
  }
}
  • 程序次序規(guī)則
    在一個(gè)線程中,按照代碼的順序叫潦,前面的操作happens-before于后面的任意操作
    例如:賦值操作 x = 42 先于 v = true 執(zhí)行
  • volatile變量規(guī)則
    對(duì)一個(gè)volatile變量的寫操作happens-before與后續(xù)對(duì)這個(gè)變量的讀操作
  • 傳遞規(guī)則
    如果 A happens-before B蝇完,并且 B happens-before C,則 A happens-before C
    "x=42" happens-before 寫變量 "v=true"。(程序次序規(guī)則)
    寫變量"v=42" happens-before 讀變量"v==true"短蜕。(volatile變量規(guī)則)
    根據(jù)傳遞性規(guī)則氢架,得到結(jié)果 "x=42" happens-before 讀變量"v==true"

  • 監(jiān)視器鎖規(guī)則
    對(duì)一個(gè)鎖的解鎖 happens-before 于隨后對(duì)這個(gè)鎖的加鎖
    線程A執(zhí)行完代碼后x的值會(huì)變成12,線程B進(jìn)入代碼塊朋魔,能夠看到線程A對(duì)x的寫操作岖研,也就是線程B能夠看到x==12
synchronized (this) { // 此處自動(dòng)加鎖
    // x是共享變量,初始值=10
    if (this.x < 12) {
        this.x = 12;
    }
} // 此處自動(dòng)解鎖
  • 線程啟動(dòng)規(guī)則
    如果線程A調(diào)用線程B的start()方法來啟動(dòng)線程B铺厨,則start()操作happens-before于線程B中的任意操作
    線程A啟動(dòng)線程B之后缎玫,線程B能夠看到線程A在啟動(dòng)線程B之前的操作,在線程B中訪問到x變量的值為100
// 在線程A中初始化線程B
Thread threadB = new Thread(() -> {
    // 此處的變量x的值是多少呢解滓?答案是100
});
// 線程A在啟動(dòng)線程B之前將共享變量x的值修改為100
x = 100;
// 啟動(dòng)線程B
threadB.start();
  • 線程終結(jié)規(guī)則
    線程A等待線程B完成(在線程A中調(diào)用線程B的join()方法實(shí)現(xiàn))赃磨,當(dāng)線程B完成后,線程A能夠訪問到線程B對(duì)共享變量的操作
Thread threadB = new Thread(() -> {
    // 在線程B中洼裤,將共享變量x的值修改為100
    x = 100;
});
// 在線程A中啟動(dòng)線程B
threadB.start();
// 在線程A中等待線程B執(zhí)行完成
threadB.join();
// 此處訪問共享變量x的值為100
  • 線程中斷規(guī)則
    對(duì)線程interrupt()方法的調(diào)用happens-before于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生
// 在線程A中將x變量的值初始化為0
private int x = 0;

public void execute() {
    // 在線程A中初始化線程B
    Thread threadB = new Thread(() -> {
        // 線程B檢測(cè)自己是否被中斷
        if (Thread.currentThread().isInterrupted()) {
            // 如果線程B被中斷邻辉,則此時(shí)x的值為100
            System.out.println(x);
        }
    });
    // 在線程A中啟動(dòng)線程B
    threadB.start();
    // 在線程A中將共享變量x的值修改為100
    x = 100;
    // 在線程A中中斷線程B
    threadB.interrupt();
}
  • 對(duì)象終結(jié)規(guī)則
    一個(gè)對(duì)象的初始化完成happens-before于它的finalize()方法的開始
public class TestThread {
    public TestThread() {
        System.out.println("構(gòu)造方法");
    }

    @Override
    public void finalize() throws Throwable {
        System.out.println("對(duì)象銷毀");
    }

    public static void main(String[] args) {
        new TestThread();
        System.gc();
    }
}

運(yùn)行結(jié)果

構(gòu)造方法
對(duì)象銷毀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市腮鞍,隨后出現(xiàn)的幾起案子值骇,更是在濱河造成了極大的恐慌,老刑警劉巖移国,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吱瘩,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡迹缀,警方通過查閱死者的電腦和手機(jī)使碾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來祝懂,“玉大人票摇,你說我怎么就攤上這事⊙馀睿” “怎么了矢门?”我有些...
    開封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)灰蛙。 經(jīng)常有香客問我祟剔,道長(zhǎng),這世上最難降的妖魔是什么摩梧? 我笑而不...
    開封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任物延,我火速辦了婚禮,結(jié)果婚禮上障本,老公的妹妹穿的比我還像新娘教届。我一直安慰自己,他們只是感情好驾霜,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開白布案训。 她就那樣靜靜地躺著,像睡著了一般粪糙。 火紅的嫁衣襯著肌膚如雪强霎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天蓉冈,我揣著相機(jī)與錄音城舞,去河邊找鬼。 笑死寞酿,一個(gè)胖子當(dāng)著我的面吹牛家夺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播伐弹,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼拉馋,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了惨好?” 一聲冷哼從身側(cè)響起煌茴,我...
    開封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎日川,沒想到半個(gè)月后蔓腐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡龄句,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年回论,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撒璧。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡透葛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出卿樱,到底是詐尸還是另有隱情僚害,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布繁调,位于F島的核電站萨蚕,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蹄胰。R本人自食惡果不足惜岳遥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望裕寨。 院中可真熱鬧浩蓉,春花似錦派继、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至认轨,卻和暖如春绅络,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嘁字。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來泰國打工恩急, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人纪蜒。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓衷恭,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親纯续。 傳聞我的和親對(duì)象是個(gè)殘疾皇子匾荆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

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