Java 內存模型

https://blog.csdn.net/qq_24535745/article/details/89674479
一、關于內存模型

1.在計算機世界中, 為了保證共享內存的正確性(原子性、可見性、有序性), 內存模型定義了共享內存系統(tǒng)中多線程程序讀寫操作行為的規(guī)范。通過這些規(guī)則來規(guī)范對內存的讀寫操作, 從而保證指令執(zhí)行的正確性。它與處理器有關、與緩存有關、與并發(fā)有關、有編譯有關。它解決了CPU多級緩存、處理器優(yōu)化、指令重排等導致的訪問問題, 保證了并發(fā)場景下的有序性、一致性、原子性。

2.內存模型解決并發(fā)問題主要采用兩種方式: 限制處理器優(yōu)化和使用內存屏障。

所以,再來總結下,JMM是一種規(guī)范,目的是解決由于多線程通過共享內存進行通信時,存在的本地內存數據不一致、編譯器會對代碼指令重排序、處理器會對代碼亂序執(zhí)行等帶來的問題。

我們說,并發(fā)編程,為了保證數據的安全,需要滿足以下三個特性:

原子性是指在一個操作中就是cpu不可以在中途暫停然后再調度,既不被中斷操作,要不執(zhí)行完成,要不就不執(zhí)行。

可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

有序性即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。

3.我們知道, Java的多線程之間是通過共享內存進行通信的, 而由于采用共享內存進行通信, 在通信過程中會存在一系列如可見性、原子性、順序性等問題, 而JMM(Java Memory Model)就是圍繞著多線程通信以及與其相關的一系列特性而建立的模型。它只是一個抽象的概念, 是一種符合內存模型規(guī)范的,屏蔽了各種硬件和操作系統(tǒng)的訪問差異的,保證了Java程序在各種平臺下對內存的訪問都能保證效果一致的機制及規(guī)范。JMM定義了一些語法集, 這些語法集映射到Java語言中就是volatile、synchronized等關鍵字。


二、內存模型的實現

1.原子性

在 Java 中,為了保證原子性,提供了兩個高級的字節(jié)碼指令 Monitorenter 和 Monitorexit。

在 Synchronized 的實現原理文章中,介紹過,這兩個字節(jié)碼,在 Java 中對應的關鍵字就是 Synchronized。

因此,在 Java 中可以使用 Synchronized 來保證方法和代碼塊內的操作是原子性的。

2.可見性

Java 內存模型是通過在變量修改后將新值同步回主內存,在變量讀取前從主內存刷新變量值的這種依賴主內存作為傳遞媒介的方式來實現的。

Java 中的 Volatile 關鍵字提供了一個功能,那就是被其修飾的變量在被修改后可以立即同步到主內存。

被其修飾的變量在每次使用之前都從主內存刷新。因此,可以使用 Volatile 來保證多線程操作時變量的可見性。

除了 Volatile,Java 中的 Synchronized 和 Final 兩個關鍵字也可以實現可見性。只不過實現方式不同,這里不再展開了。

3.有序性

在 Java 中,可以使用 Synchronized 和 Volatile 來保證多線程之間操作的有序性。

實現方式有所區(qū)別:Volatile 關鍵字會禁止指令重排。Synchronized 關鍵字保證同一時刻只允許一條線程操作。

4.內存間交互操作

關于主內存與工作內存之間的具體交互協(xié)議,即一個變量如何從主內存拷貝到工作內存、如何從工作內存同步到主內存之間的實現細節(jié),Java內存模型定義了以下八種操作來完成:

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

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

read(讀取):作用于主內存變量,把一個變量值從主內存?zhèn)鬏數骄€程的工作內存中,以便隨后的load動作使用

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

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

assign(賦值):作用于工作內存的變量,它把一個從執(zhí)行引擎接收到的值賦值給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。

store(存儲):作用于工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨后的write的操作。

write(寫入):作用于主內存的變量,它把store操作從工作內存中一個變量的值傳送到主內存的變量中。

結論:讀者可能發(fā)現了,好像 Synchronized 關鍵字是萬能的,它可以同時滿足以上三種特性,這也是很多人濫用 Synchronized 的原因。

但是 Synchronized 是比較影響性能的,雖然編譯器提供了很多鎖優(yōu)化技術,但是也不建議過度使用。

三、內存模型的基礎原理

1.指令重排序

在執(zhí)行程序時,為了提高性能,編譯器和處理器會對指令做重排序。但是,JMM確保在不同的編譯器和不同的處理器平臺之上,通過插入特定類型的Memory Barrier來禁止特定類型的編譯器重排序和處理器重排序,為上層提供一致的內存可見性保證。

編譯器優(yōu)化重排序:編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序。

指令級并行的重排序:如果不存l在數據依賴性,處理器可以改變語句對應機器指令的執(zhí)行順序。

內存系統(tǒng)的重排序:處理器使用緩存和讀寫緩沖區(qū),這使得加載和存儲操作看上去可能是在亂序執(zhí)行。

2.數據依賴性

如果兩個操作訪問同一個變量,其中一個為寫操作,此時這兩個操作之間存在數據依賴性。

編譯器和處理器不會改變存在數據依賴性關系的兩個操作的執(zhí)行順序,即不會重排序。

3.as-if-serial

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

4.內存屏障(Memory Barrier )

上面講到了,通過內存屏障可以禁止特定類型處理器的重排序,從而讓程序按我們預想的流程去執(zhí)行。內存屏障,又稱內存柵欄,是一個CPU指令,基本上它是一條這樣的指令:

保證特定操作的執(zhí)行順序。

影響某些數據(或則是某條指令的執(zhí)行結果)的內存可見性。

編譯器和CPU能夠重排序指令,保證最終相同的結果,嘗試優(yōu)化性能。插入一條Memory Barrier會告訴編譯器和CPU:不管什么指令都不能和這條Memory Barrier指令重排序。

Memory Barrier所做的另外一件事是強制刷出各種CPU cache,如一個Write-Barrier(寫入屏障)將刷出所有在Barrier之前寫入 cache 的數據,因此,任何CPU上的線程都能讀取到這些數據的最新版本。

這和java有什么關系?上面java內存模型中講到的volatile是基于Memory Barrier實現的。

如果一個變量是volatile修飾的,JMM會在寫入這個字段之后插進一個Write-Barrier指令,并在讀這個字段之前插入一個Read-Barrier指令。這意味著,如果寫入一個volatile變量,就可以保證:

一個線程寫入變量a后,任何線程訪問該變量都會拿到最新值。

在寫入變量a之前的寫入操作,其更新的數據對于其他線程也是可見的。因為Memory Barrier會刷出cache中的所有先前的寫入。

5.先行發(fā)生原則(happens-before)

從jdk5開始,java使用新的JSR-133內存模型,基于happens-before的概念來闡述操作之間的內存可見性。

在JMM中,如果一個操作的執(zhí)行結果需要對另一個操作可見,那么這兩個操作之間必須要存在happens-before關系,這個的兩個操作既可以在同一個線程,也可以在不同的兩個線程中。

與程序員密切相關的happens-before規(guī)則如下:

程序順序規(guī)則:一個線程中的每個操作,happens-before于該線程中任意的后續(xù)操作。

監(jiān)視器鎖規(guī)則:對一個鎖的解鎖操作,happens-before于隨后對這個鎖的加鎖操作。

volatile域規(guī)則:對一個volatile域的寫操作,happens-before于任意線程后續(xù)對這個volatile域的讀。

傳遞性規(guī)則:如果 A happens-before B,且 B happens-before C,那么A happens-before C。

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

四、內存模型中關鍵字

內存模型中包含了幾個關鍵字:volatile、final和synchronized幫助程序員把代碼中的并發(fā)需求描述給編譯器。Java內存模型中定義了它們的行為,確保正確同步的Java代碼在所有的處理器架構上都能正確執(zhí)行。

1.synchronization 可以實現什么

Synchronization有多種語義,其中最容易理解的是互斥,對于一個monitor對象,只能夠被一個線程持有,意味著一旦有線程進入了同步代碼塊,那么其它線程就不能進入直到第一個進入的線程退出代碼塊(這因為都能理解)。

但是更多的時候,使用synchronization并非單單互斥功能,Synchronization保證了線程在同步塊之前或者期間寫入動作,對于后續(xù)進入該代碼塊的線程是可見的(又是可見性,不過這里需要注意是對同一個monitor對象而言)。在一個線程退出同步塊時,線程釋放monitor對象,它的作用是把CPU緩存數據(本地緩存數據)刷新到主內存中,從而實現該線程的行為可以被其它線程看到。在其它線程進入到該代碼塊時,需要獲得monitor對象,它在作用是使CPU緩存失效,從而使變量從主內存中重新加載,然后就可以看到之前線程對該變量的修改。

但從緩存的角度看,似乎這個問題只會影響多處理器的機器,對于單核來說沒什么問題,但是別忘了,它還有一個語義是禁止指令的重排序,對于編譯器來說,同步塊中的代碼不會移動到獲取和釋放monitor外面。

下面這種代碼,千萬不要寫,會讓人笑掉大牙:


這實際上是沒有操作的操作,編譯器完成可以刪除這個同步語義,因為編譯知道沒有其它線程會在同一個monitor對象上同步。

所以,請注意:對于兩個線程來說,在相同的monitor對象上同步是很重要的,以便正確的設置happens-before關系。

2.final 可以影響什么

如果一個類包含final字段,且在構造函數中初始化,那么正確的構造一個對象后,final字段被設置后對于其它線程是可見的。

這里所說的正確構造對象,意思是在對象的構造過程中,不允許對該對象進行引用,不然的話,可能存在其它線程在對象還沒構造完成時就對該對象進行訪問,造成不必要的麻煩。


上面例子描述了應該如何使用final字段,一個線程A執(zhí)行reader方法,如果f已經在線程B初始化好,那么可以確保線程A看到x值是3,因為它是final修飾的,而不能確??吹統(tǒng)的值是4。

如果構造函數是下面這樣的:


這樣通過global.obj拿到對象后,并不能保證x的值是3.

3.volatile可以做什么

Volatile字段主要用于線程之間進行通信,volatile字段的每次讀行為都能看到其它線程最后一次對該字段的寫行為,通過它就可以避免拿到緩存中陳舊數據。它們必須保證在被寫入之后,會被刷新到主內存中,這樣就可以立即對其它線程可以見。類似的,在讀取volatile字段之前,緩存必須是無效的,以保證每次拿到的都是主內存的值,都是最新的值。volatile的內存語義和sychronize獲取和釋放monitor的實現目的是差不多的。

對于重新排序,volatile也有額外的限制。

下面看一個例子:


同樣的,假設一個線程A執(zhí)行writer,另一個線程B執(zhí)行reader,writer中對變量v的寫入把x的寫入也刷新到主內存中。reader方法中會從主內存重新獲取v的值,所以如果線程B看到v的值為true,就能保證拿到的x是42.(因為把x設置成42發(fā)生在把v設置成true之前,volatile禁止這兩個寫入行為的重排序)。

如果變量v不是volatile,那么以上的描述就不成立了,因為執(zhí)行順序可能是v=true, x=42,或者對于線程B來說,根本看不到v被設置成了true。

4.double-checked locking的問題


臭名昭著的雙重檢查(其中一種單例模式),是一種延遲初始化的實現技巧,避免了同步的開銷,因為在早期的JVM,同步操作性能很差,所以才出現了這樣的小技巧。

這個技巧看起來很聰明,避免了同步的開銷,但是有一個問題,它可能不起作用,為什么呢?因為實例的初始化和實例字段的寫入可能被編譯器重排序,這樣就可能返回部門構造的對象,結果就是讀到了一個未初始化完成的對象。

當然,這種bug可以通過使用volatile修飾instance字段進行fix,但是我覺得這種代碼格式實在太丑陋了,如果真要延遲初始化實例,不妨使用下面這種方式:


由于是靜態(tài)字段的初始化,可以確保對訪問該類的所以線程都是可見的。

總結

1.我們應該清楚知道,jmm就是一組規(guī)則,這組規(guī)則意在解決在并發(fā)編程可能出現的線程安全問題,并提供了內置解決方案(happen-before原則)及其外部可使用的同步手段(synchronized/volatile等),確保了程序執(zhí)行在多線程環(huán)境中的應有的原子性,可視性及其有序性。

2.jvm和jmm之間的關系:jmm中的主內存、工作內存與jvm中的Java堆、棧、方法區(qū)等并不是同一個層次的內存劃分,這兩者基本上是沒有關系的,如果兩者一定要勉強對應起來,那從變量、主內存、工作內存的定義來看,主內存主要對應于Java堆中的對象實例數據部分,而工作內存則對應于虛擬機棧中的部分區(qū)域。從更低層次上說,主內存就直接對應于物理硬件的內存,而為了獲取更好的運行速度,虛擬機(甚至是硬件系統(tǒng)本身的優(yōu)化措施)可能會讓工作內存優(yōu)先存儲于寄存器和高速緩存中,因為程序運行時主要訪問讀寫的是工作內存。

3.JVM內存結構, 和Java虛擬機的運行時區(qū)域有關。Java內存模型, 和Java的并發(fā)編程有關。Java對象模型, 和Java對象在虛擬機中的表現形式有關。

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

相關閱讀更多精彩內容

  • Java內存區(qū)域 Java虛擬機在運行程序時會把其自動管理的內存劃分為以上幾個區(qū)域,每個區(qū)域都有的用途以及創(chuàng)建銷毀...
    架構師springboot閱讀 1,942評論 0 5
  • 除了充分利用計算機處理器的能力外,一個服務端同時對多個客戶端提供服務則是另一個更具體的并發(fā)應用場景。衡量一個服務性...
    胡二囧閱讀 1,462評論 0 12
  • 概述 Java的內存模型(Java Memory Model )簡稱JMM。首先應該明白,Java內存模型是一個規(guī)...
    亂敲代碼閱讀 335評論 0 0
  • 第一步,了解JVM基本概念,基本結構。 第二步,了解JVM中線程私有區(qū)和公有區(qū)。 第三步,了解線程與Java內存模...
    Arya鑫閱讀 1,305評論 0 10
  • JMM簡介 Java的內存模型JMM(Java MemoryModel)JMM主要是為了規(guī)定了線程和內存之間的一些...
    團長plus閱讀 1,359評論 0 2

友情鏈接更多精彩內容