【JAVA提升】- Java內(nèi)存模型

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

Java虛擬機(jī)規(guī)范中定義了Java內(nèi)存模型(Java Memory Model,JMM),用于屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,
以實現(xiàn)讓Java程序在各種平臺下都能達(dá)到一致的并發(fā)效果,JMM規(guī)范了Java虛擬機(jī)與計算機(jī)內(nèi)存是如何協(xié)同工作的:
規(guī)定了一個線程如何以及何時可以看到由其他線程修改過后的共享變量的值,以及在必須時如何同步的訪問共享變量。

2 硬件架構(gòu)

2.1 硬件架構(gòu)介紹

物理硬件架構(gòu)如下圖。

jmm-hardware.jpg
  • 多CPU: 一個現(xiàn)代計算機(jī)通常由兩個或者多個CPU。其中一些CPU還有多核。從這一點可以看出,在一個有兩個或者多個CPU的現(xiàn)代計算機(jī)上同時運行多個線程是可能的。每個CPU在某一時刻運行一個線程是沒有問題的。這意味著,如果你的Java程序是多線程的,在你的Java程序中每個CPU上一個線程可能同時(并發(fā))執(zhí)行。

  • CPU寄存器: 每個CPU都包含一系列的寄存器,它們是CPU內(nèi)內(nèi)存的基礎(chǔ)。CPU在寄存器上執(zhí)行操作的速度遠(yuǎn)大于在主存上執(zhí)行的速度。這是因為CPU訪問寄存器的速度遠(yuǎn)大于主存。

  • 高速緩存cache: 由于計算機(jī)的存儲設(shè)備與處理器的運算速度之間有著幾個數(shù)量級的差距,所以現(xiàn)代計算機(jī)系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運算速度的高速緩存(Cache)來作為內(nèi)存與處理器之間的緩沖:將運算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運算能快速進(jìn)行,當(dāng)運算結(jié)束后再從緩存同步回內(nèi)存之中,這樣處理器就無須等待緩慢的內(nèi)存讀寫了。CPU訪問緩存層的速度快于訪問主存的速度,但通常比訪問內(nèi)部寄存器的速度還要慢一點。每個CPU可能有一個CPU緩存層,一些CPU還有多層緩存。在某一時刻,一個或者多個緩存行(cache lines)可能被讀到緩存,一個或者多個緩存行可能再被刷新回主存。

  • 內(nèi)存: 一個計算機(jī)還包含一個主存。所有的CPU都可以訪問主存。主存通常比CPU中的緩存大得多。

  • 運作原理: 通常情況下,當(dāng)一個CPU需要讀取主存時,它會將主存的部分讀到CPU緩存中。它甚至可能將緩存中的部分內(nèi)容讀到它的內(nèi)部寄存器中,然后在寄存器中執(zhí)行操作。當(dāng)CPU需要將結(jié)果寫回到主存中去時,它會將內(nèi)部寄存器的值刷新到緩存中,然后在某個時間點將值刷新回主存。
    線程之間想要數(shù)據(jù)能共享,則需要通過數(shù)據(jù)刷新(更新)主內(nèi)存進(jìn)行溝通。

2.2 硬件架構(gòu)導(dǎo)致的問題

  • 緩存一致性問題: 在多處理器系統(tǒng)中,每個處理器都有自己的高速緩存,而它們又共享同一主內(nèi)存(MainMemory)。
    基于高速緩存的存儲交互很好地解決了處理器與內(nèi)存的速度矛盾,但是也引入了新的問題:緩存一致性(CacheCoherence)。
    當(dāng)多個處理器的運算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時,將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致的情況,
    如果真的發(fā)生這種情況,那同步回到主內(nèi)存時以誰的緩存數(shù)據(jù)為準(zhǔn)呢?為了解決一致性的問題,需要各個處理器訪問緩存時都遵循一些協(xié)議,
    在讀寫時要根據(jù)協(xié)議來進(jìn)行操作,這類協(xié)議有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等:

  • 指令重排序問題: 為了使得處理器內(nèi)部的運算單元能盡量被充分利用,處理器可能會對輸入代碼進(jìn)行亂序執(zhí)行(Out-Of-Order Execution)優(yōu)化,
    處理器會在計算之后將亂序執(zhí)行的結(jié)果重組,保證該結(jié)果與順序執(zhí)行的結(jié)果是一致的,但并不保證程序中各個語句計算的先后順序與輸入代碼中的順序一致。
    因此,如果存在一個計算任務(wù)依賴另一個計算任務(wù)的中間結(jié)果,那么其順序性并不能靠代碼的先后順序來保證。
    與處理器的亂序執(zhí)行優(yōu)化類似,Java虛擬機(jī)的即時編譯器中也有類似的指令重排序(Instruction Reorder)優(yōu)化

3 JMM作為溝通橋梁

3.1 JMM的架構(gòu)

JMM為了解決上面的問題,對cpu的寄存器和高速緩存進(jìn)行抽象描述,進(jìn)行管理。JMM中,線程之間的通訊表現(xiàn)為如下圖中的方式:

jmm.jpg
  • 線程之間的共享變量存儲在主內(nèi)存(Main Memory)中
  • 每個線程都有一個私有的本地內(nèi)存(Local Memory),本地內(nèi)存是JMM的一個抽象概念,并不真實存在,它涵蓋了緩存、寫緩沖區(qū)、寄存器以及其他的硬件和編譯器優(yōu)化。本地內(nèi)存中存儲了該線程以讀/寫共享變量的拷貝副本。
  • Java內(nèi)存模型中的線程的工作內(nèi)存(working memory)是cpu的寄存器和高速緩存的抽象描述。JVM的內(nèi)存結(jié)構(gòu)描述的是內(nèi)存的劃分,與工作內(nèi)存概念不同
  • 主內(nèi)存就是硬件的內(nèi)存,而為了獲取更好的運行速度,虛擬機(jī)及硬件系統(tǒng)可能會讓工作內(nèi)存優(yōu)先存儲于寄存器和高速緩存中。

3.2 JMM下線程通過的過程

如果線程A與線程B之間要通信的話,必須要經(jīng)歷下面2個步驟:

1)線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去。

2)線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量,獲取到最新的值。

而要保證這個過程的數(shù)據(jù)一致性,JMM定義了一列的操作來完成

  • lock(鎖定) :作用于主內(nèi)存的變量,把一個變量標(biāo)識為一條線程獨占狀態(tài)。
  • unlock(解鎖) :作用于主內(nèi)存變量,把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
  • 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í)行引擎,每當(dāng)虛擬機(jī)遇到一個需要使用變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。
  • assign(賦值) :作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
  • store(存儲) :作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個變量的值傳送到主內(nèi)存中,以便隨后的write的操作。
  • write(寫入) :作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中一個變量的值傳送到主內(nèi)存的變量中。

而以上操作又設(shè)置了一些規(guī)則

  • 如果要把一個變量從主內(nèi)存中復(fù)制到工作內(nèi)存,就需要按順尋地執(zhí)行read和load操作, 如果把變量從工作內(nèi)存中同步回主內(nèi)存中,就要按順序地執(zhí)行store和write操作。但Java內(nèi)存模型只要求上述操作必須按順序執(zhí)行,而沒有保證必須是連續(xù)執(zhí)行。
  • 不允許read和load、store和write操作之一單獨出現(xiàn)
  • 不允許一個線程丟棄它的最近assign的操作,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中。
  • 不允許一個線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中。
  • 一個新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個未被初始化(load或assign)的變量。即就是對一個變量實施use和store操作之前,必須先執(zhí)行過了assign和load操作。
  • 一個變量在同一時刻只允許一條線程對其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會被解鎖。lock和unlock必須成對出現(xiàn)
  • 如果對一個變量執(zhí)行l(wèi)ock操作,將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值
  • 如果一個變量事先沒有被lock操作鎖定,則不允許對它執(zhí)行unlock操作;也不允許去unlock一個被其他線程鎖定的變量。
  • 對一個變量執(zhí)行unlock操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)。

3.3 JMM解決的問題

  1. 多線程讀同步與可見性

  2. 多線程寫同步與原子性


參考鏈接

?著作權(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ù)。

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