JMM Java 內(nèi)存模型

Java 內(nèi)存模型

Java Memory Model , 為 java 內(nèi)存模型, 簡(jiǎn)稱為 JMM .

參考鏈接

深入理解java內(nèi)存模型系列文章

主要參考為上述鏈接,上述講的特別好,很清楚,很詳細(xì)。

JMM 解決可見性的問題(同步包括獨(dú)占性和可見性)

Java 內(nèi)存模型的主要目的是定義程序中各個(gè)變量的訪問規(guī)則,即在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量。

兩個(gè)名詞含義:

主內(nèi)存: 線程之間共享變量存儲(chǔ)在主內(nèi)存中

工作內(nèi)存: 每個(gè)線程都有一個(gè)私有的本地內(nèi)存, 稱為工作內(nèi)存,里面存放著該線程以讀/寫共享變量的副本.

JMM 同時(shí)定義了線程和主內(nèi)存之間的抽象關(guān)系.

  1. JMM 通過控制主內(nèi)存與每個(gè)線程的本地內(nèi)存之間的交互, 提供了內(nèi)存可見性保證。

  2. Java 線程間的通信由 JMM 控制

    JMM 決定一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)另一個(gè)線程可見

  3. Java 內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存中,每條線程還有自己的工作內(nèi)存(與cache類比)

    線程對(duì)變量的所有操作(讀取、賦值)都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫內(nèi)存中的變量。

    Java 線程 <-----> 工作內(nèi)存 <------> savaload 操作 <-----> 主內(nèi)存

主內(nèi)存和工作內(nèi)存之間的具體交互協(xié)議

Java 內(nèi)存模型定義了一下八種操作:

  1. lock : ----> 主內(nèi)存變量, 把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài)
  2. Unlock : ----> 主內(nèi)存變量, 釋放
  3. read : ----> 主內(nèi)存變量, 把一個(gè)變量值從內(nèi)存?zhèn)魉偷骄€程的工作內(nèi)存 ,便于load 操作
  4. load : ----> 工作內(nèi)存的變量,把read的變量值放入到工作內(nèi)存的變量副本中
  5. use : ----> 工作內(nèi)存的變量,工作內(nèi)存的變量值--> 執(zhí)行引擎
  6. assign : ----> 工作內(nèi)存的變量, 把從執(zhí)行引擎得到的值----> 賦給工作內(nèi)存的變量
  7. store : ----> 工作內(nèi)存的變量, 傳給主內(nèi)存,便于后面的write操作
  8. write : ----> 主內(nèi)存的變量,把從工作內(nèi)存的傳過來的變量進(jìn)行write進(jìn)主內(nèi)存

Java內(nèi)存模型只要求上述操作必須按順序執(zhí)行,而沒有保證必須是連續(xù)執(zhí)行

happens-before 原則

JMM 中,如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另外一個(gè)操作可見,那么這兩個(gè)操作之間必需要存在 happens-before 關(guān)系.

上述兩個(gè)操作,可在同一個(gè)線程里,也可在不同的線程中.

規(guī)則如下:

  1. 程序順序規(guī)則:

    一個(gè)線程中的每個(gè)操作,happens- before 作用于該線程中的任意后續(xù)操作.

  2. 監(jiān)視器規(guī)則

    對(duì)一個(gè)監(jiān)視器鎖的解鎖,happens- before 作用于隨后對(duì)這個(gè)監(jiān)視器鎖的加鎖.

  3. volatile 變量原則

    對(duì)一個(gè) volatile 域的寫,happens- before 作用于任意后續(xù)對(duì)這個(gè) volatile 域的讀

  4. 傳遞性

    如果 A happens- before B,且 B happens- before C,那么 A happens- before C.

對(duì) happens- before 的理解

兩個(gè)操作之間存在 happens- before, 并不是意味著 前一個(gè)操作必需在后一個(gè)操作執(zhí)行之前執(zhí)行, 而是僅僅要求前一個(gè)操作的結(jié)果(執(zhí)行后的結(jié)果),對(duì)后一個(gè)操作可見, 且前一個(gè)操作按順序排在第二個(gè)操作之前(代碼間存在重排序問題).

重排序

源代碼----> 編譯器優(yōu)化重排序 ----> 指令級(jí)并行重排序 -----> 內(nèi)存系統(tǒng)重排序 ----> 最終執(zhí)行的指令序列

為了保證內(nèi)存的可見性,Java編譯器在生成指令序列的適當(dāng)位置會(huì)插入內(nèi)存屏障指令來禁止特定類型的處理器重排序 , 因?yàn)橹嘏判蚩赡軙?huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見性問題。

JMM 特點(diǎn)

  1. Java內(nèi)存模型JMM 確保在不同的編輯器和不同的處理器平臺(tái)上,通過禁止特定的的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見性。

  2. Java內(nèi)存模型JMM 不會(huì)破壞已有的 數(shù)據(jù)依賴性, as-if-serial 語義, 程序順序規(guī)則. (具體含義見下面)

Java內(nèi)存模型把內(nèi)存屏障分為L(zhǎng)oadLoad、LoadStore、StoreLoad和StoreStore四種:

image

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

編譯器和處理器在重排序時(shí),會(huì)遵守?cái)?shù)據(jù)依賴性。

  • 數(shù)據(jù)依賴性僅針對(duì)單個(gè)處理器中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮

as-if-serial 語義

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

happens- before 程序順序規(guī)則

JMM僅僅要求前一個(gè)操作(執(zhí)行的結(jié)果)對(duì)后一個(gè)操作可見,且前一個(gè)操作按順序排在第二個(gè)操作之前

JMM允許不影響操作結(jié)果的重排序

重排序?qū)Χ嗑€程的影響

  • 多線程程序中,對(duì)存在控制依賴的操作重排序,可能會(huì)改變程序的執(zhí)行結(jié)果。

  • 單線程程序中, 對(duì)存在控制依賴的操作重排序,不會(huì)改變執(zhí)行結(jié)果。

數(shù)據(jù)競(jìng)爭(zhēng)與順序一致性保證

當(dāng)程序未正確同步時(shí),就會(huì)存在數(shù)據(jù)競(jìng)爭(zhēng)

在Java內(nèi)存模型對(duì)數(shù)據(jù)競(jìng)爭(zhēng)的定義如下:

  1. 在一個(gè)線程中寫一個(gè)變量
  2. 在另一個(gè)線程中讀同一個(gè)變量
  3. 寫和讀沒有通過同步來排序

如果一個(gè)多線程程序能正確同步,這個(gè)程序?qū)⑹且粋€(gè)沒有數(shù)據(jù)競(jìng)爭(zhēng)的程序,正確同步的多線程程序

JMM 對(duì)正確同步的多線程程序做了如下保證:

  • 如果程序是正確同步的,則程序的執(zhí)行將具有順序一致性, 即程序的執(zhí)行結(jié)果與該程序在順序一致性內(nèi)存模型中的執(zhí)行結(jié)果相同,

順序一致性內(nèi)存模型

順序一致性內(nèi)存模型 是一個(gè)被計(jì)算機(jī)科學(xué)家理想化了的理論參考模型,提供了極強(qiáng)的內(nèi)存可見性保證

與Java 內(nèi)存模型不同
特性:

  • 一個(gè)線程中的所有操作必須按照程序的順序來執(zhí)行
  • (不管程序是否同步)所有線程都只能看到一個(gè)單一的操作執(zhí)行順序
  • 在順序一致性內(nèi)存模型中,每個(gè)操作都必須原子執(zhí)行且立刻對(duì)所有線程可見。

同步機(jī)制 :

volatile , synchronized, final

  1. volatile 關(guān)鍵字

    volatile修飾的變量,線程在每次使用變量的時(shí)候,都會(huì)讀取修改后的最新的值.

    對(duì)變量的寫操作,不依賴于當(dāng)前值

    不能和其他變量同時(shí)出現(xiàn)在一個(gè)表達(dá)式中

    volatile的作用就是使它修飾的變量的讀寫操作都必須在內(nèi)存中進(jìn)行

    • 可見性:對(duì)一個(gè) volatile 變量的讀,總是能看到(任意線程)對(duì)這個(gè) volatile 變量最后的寫入.
    • 原子性:對(duì)任意 volatile變量的讀/寫都具有原子性

    volatile 變量的寫-讀可以實(shí)現(xiàn)線程之間的通信, 如下:

    • 線程A寫一個(gè)volatile變量, 實(shí)質(zhì)上是線程A向接下來將要讀這個(gè)volatile變量的某個(gè)線程發(fā)出了(其對(duì)共享變量所在修改的)消息
    • 線程B讀一個(gè)volatile變量,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(在寫這個(gè)volatile變量之前對(duì)共享變量所做修改的)消息
    • 線程A寫一個(gè)volatile變量,隨后線程B讀這個(gè)volatile變量,這個(gè)過程實(shí)質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息

    volatile 變量的寫-讀 的內(nèi)存意義

    • 當(dāng)寫一個(gè) volatile 變量時(shí), JMM 會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存;

    • 當(dāng)讀一個(gè) volatile 變量時(shí), JMM 會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效,線程接下來將從主內(nèi)存中讀取共享變量.

    為了達(dá)到上述的內(nèi)存意義效果,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序, 如下,采用保守策略的 JMM 內(nèi)存屏障插入策略:

    • 在每個(gè) volatile 寫操作的前面插入一個(gè) StoreStore 屏障
    • 在每個(gè) volatile 寫操作的后面插入一個(gè) StoreLoad 屏障
    • 在每個(gè) volatile 讀操作的后面插入一個(gè) LoadLoad 屏障
    • 在每個(gè) volatile 讀操作的后面插入一個(gè) LoadStore 屏障
  1. synchronized 關(guān)鍵字

    線程同步的內(nèi)部機(jī)制,通過加鎖的方式保證線程的可見性和互斥性,
    即一個(gè)線程的執(zhí)行結(jié)果可以被另一個(gè)線程所看到且在同一時(shí)間只能有一個(gè)線程執(zhí)行被保護(hù)的代碼塊.

    鎖釋放和獲取的內(nèi)存語義:

    • 當(dāng)線程釋放鎖時(shí),JMM 會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中

    • 當(dāng)線程獲取鎖時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效,從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須要從主內(nèi)存中去讀取共享變量

    如下消息傳遞:

    • 線程A釋放一個(gè)鎖,實(shí)質(zhì)上是線程A向接下來將要獲取這個(gè)鎖的某個(gè)線程發(fā)出了(線程A對(duì)共享變量所做修改的)消息;
    • 線程B獲取一個(gè)鎖,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(在釋放這個(gè)鎖之前對(duì)共享變量所做修改的)消息;
    • 線程A釋放鎖,隨后線程B獲取這個(gè)鎖,這個(gè)過程實(shí)質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息
  2. final 關(guān)鍵字

    與前面的鎖和volatile相比較,對(duì)final域的讀和寫更像是普通的變量訪問

    • 在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)final域的寫入,與隨后把這個(gè)被構(gòu)造對(duì)象的引用賦值給一個(gè)引用變量,這兩個(gè)操作之間不能重排序。
    • 初次讀一個(gè)包含final域的對(duì)象的引用,與隨后初次讀這個(gè)final域,這兩個(gè)操作之間不能重排序

上面的兩條規(guī)則其實(shí)可分為

1.寫final域的重排序規(guī)則

  1. 讀final域的重排序規(guī)則

final 域的重排序規(guī)則

  • JMM禁止編譯器把final域的寫重排序到構(gòu)造函數(shù)之外

  • 編譯器會(huì)在 final 域的寫之后,構(gòu)造函數(shù) return 之前,插入一個(gè) StoreStore 屏障(內(nèi)存屏障)
    這個(gè)屏障禁止處理器把 final 域的寫重排序到構(gòu)造函數(shù)之外。

final 域的重排序規(guī)則

  • 在一個(gè)線程中,初次讀對(duì)象引用與初次讀該對(duì)象包含的 final 域,JMM 禁止處理器重排序這兩個(gè)操作
    僅針對(duì)與處理器(重排序的后兩種),
    編譯器會(huì)在讀final域操作的前面插入一個(gè) LoadLoad 屏障

volatile與synchronized 的同異

  1. volatile 本質(zhì)是在告訴 jvm 當(dāng)前變量在寄存器中的值是不確定的,需要從主存中讀取。
  2. synchronized 則是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問該變量,其他線程被阻塞住.
  3. volatile 僅能使用在變量級(jí)別, synchronized 則可以使用在變量,方法.
  4. volatile 僅能實(shí)現(xiàn)變量的修改可見性,但不具備原子特性,而 synchronized 則可以保證變量的修改可見性和原子性
  5. volatile 不會(huì)造成線程的阻塞,而 synchronized 可能會(huì)造成線程的阻塞.
  6. volatile 標(biāo)記的變量不會(huì)被編譯器優(yōu)化,而 synchronized 標(biāo)記的變量可以被編譯器優(yōu)化.

如有錯(cuò)誤,請(qǐng)指出,謝謝.

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

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

  • 并發(fā)系列的文章都是根據(jù)閱讀《Java 并發(fā)編程的藝術(shù)》這本書總結(jié)而來,想更深入學(xué)習(xí)的同學(xué)可以自行購(gòu)買此書進(jìn)行學(xué)習(xí)。...
    小之丶閱讀 1,116評(píng)論 1 7
  • 59. Spiral Matrix II: 這道題比上一個(gè)spiral matrix1好做,只要記錄一下每一次的t...
    健時(shí)總向亂中忙閱讀 222評(píng)論 0 0
  • 高效的晨讀后郭老師用短短的幾分鐘時(shí)間跟同學(xué)們說了一下今天的安排,讓每一個(gè)學(xué)生知道全體要做的事情,讓他們可以見縫插針...
    山茶_5c91閱讀 257評(píng)論 0 1

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