Java內存模型JMM
java內存模型定義
上一遍文章我們講到了CPU緩存一致性以及內存屏障問題。那么Java作為一個跨平台的語言,它的實現要面對不同的底層硬件系統,設計一個中間層模型來屏蔽底層的硬件差異,給上層的開發者一個一致的使用接口。Java內存模型就是這樣一個中間層的模型,它為程序員屏蔽了底層的硬件實現細節,支持大部分的主流硬件平台。
java內存模型(Java Memory Mode):java內存模型是java虛擬機內存如何與計算機內存(RAM)一起工作。java虛擬機是是整個計算機的模型,所以這個模型自然包含一個內存模型。也可以說JMM是java虛擬機內存使用規范。
通俗的來講,就是描述Java中各種變量(線程共享變量)的訪問規則,以及在JVM中將變量存儲到內存和從內存中讀取變量這樣的底層細節。
Java內存模型規定了不同線程如何以及何時可以看到其他線程寫入共享變量的值以及如何在必要時同步對共享變量的訪問。
注意:Java Memory Model並不是真實存在的,他只是物理內存模型的一個映射。
Java 內存模型介紹

JVM中內存分配的兩個概念:
-
stack(棧)
特點: 存取速度快、對象生命周期確定、數據大小確定。
存儲數據:基本類型變量、對象引用(句柄)
位置:緩存、寄存器、寫緩沖區。 -
heap(堆)
特點: 存取速度慢、運行時動態分配大小、對象生命抽周期不確定、垃圾回收。
存儲數據:對象
位置:主內存、緩存
理論上說所有的stack和heap都存儲在物理主內存中,但隨着CPU運算其數據的副本可能被緩存或者寄存器持有,持有的數據遵從一致性協議.
存儲方式
上面說到,一個對象時存儲在heap上的,對象中所屬的方法與方法的成員變量存儲在stack上。一個對象的成員變量隨着對象本身存儲在堆上,無論該對象類型是引用類型或者是基本類型。 靜態變量和對象類定義存儲於堆上。
並發原因
存儲在堆上的對象可以被持有該對象引用的棧訪問。能訪問對象,也就能訪問該對象中的成員變量。當了兩個線程同時訪問一個對象時,每個線程都擁有該對象成員變量的私有拷貝。
這里只是粗略分配了java內存模型。具體細節的內存分配請查看
JVM內存管理概述
Java內存模型與系統內存模型
我們來看看一個關系圖:

在系統內存架構中並沒有棧(stack)、堆(heap)這種概念,只有寄存器(register)、緩存(cache)、主內存(RAM、Main Memory)。理論上說所有的棧和堆都存儲在主內存中,但隨着CPU運算其數據的副本可能被緩存或者寄存器持有。持有的數據遵從CPU-Cache一致性協議。
CPU內存模型、一致性協議可以參考前一篇文章死磕並發之CPU緩存一致性協議(MESI)
Java 內存模型抽象結構圖

主內存:保存了所有的變量。
共享變量:如果一個變量被多個線程使用,那么這個變量會在每個線程的工作內存中保有一個副本,這種變量就是共享變量。
比如成員變量、靜態變量、數組元素等。
工作內存:每個線程都有自己的工作內存,線程獨享,保存了線程用到了變量的副本(主內存共享變量的一份拷貝)。工作內存負責與線程交互,也負責與主內存交互。為了更高的效率java虛擬機、硬件系統可能讓工作內優先分配在寄存器、緩存中。
JMM對共享內存的操作做出了如下兩條規定:
- 線程對共享內存的所有操作都必須在自己的工作內存中進行,不能直接從主內存中讀寫。
- 不同線程無法直接訪問其他線程工作內存中的變量,因此共享變量的值傳遞需要通過主內存完成。
java並發問題的根源
假設線程A和線程B同事訪問某個對象的成員變量x。當線程a需要操作變量a,時會將a副本復制到線程A的工作內存中。

當線程a未執行完畢,線程b也要訪問變量a

但是線程a與線程b操作的是自己工作空間中的變量副本。 線程a中的副本和線程b中間的副本相符不可見。如果a線程率先完成了任務並寫回主存。那么線程b的運算就是在使用后臟數據運算。如果b也寫回主存那么線程a的任務就會丟失。

為了保證程序的准確性,我們就需要在並發時添加額外的同步操作。
java內存模型-內存間的八種同步操作
操作過程
我們接着再來關注下變量從主內存讀取到工作內存,然后同步回工作內存的細節,這就是主內存與工作內存之間的交互協議。Java內存模型定義了以下8種操作來完成,它們都是原子操作(除了對long和double類型的變量)。

鎖定(lock):作用於主內存中的變量,將他標記為一個線程獨享變量。
通常意義上的上鎖,就是一個線程正在使用時,其他線程必須等待該線程任務完成才能繼續執行自己的任務。
解鎖(unlock):作用於主內存中的變量,解除變量的鎖定狀態,被解除鎖定狀態的變量才能被其他線程鎖定。
執行完成后解開鎖。
read(讀取):作用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨后的load動作使用。
從主內存 讀取到工作內存中。
load(載入):把read操作從主內存中得到的變量值放入工作內存的變量的副本中。
給工作內存中的副本賦值。
use(使用):把工作內存中的一個變量的值傳給執行引擎,每當虛擬機遇到一個使用到變量的指令時都會使用該指令。
程序執行過程中讀取該值時調用。
assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
將運算完成后的新值賦回給工作內存中的變量,相當於修改工作內存中的變量。
store(存儲):作用於工作內存的變量,它把工作內存中一個變量的值傳送到主內存中,以便隨后的write操作使用。
將該值從變量中取出,寫入工作內存中。
write(寫入):作用於主內存的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中。
將工作內存中的值寫回主內存。
讀取執行步驟

寫入執行步驟

操作規則
-
不允許read和load、store和write操作之一單獨出現,即不允許一個變量從主內存讀取了但工作內存不接受,或者從工作內存發起回寫了但主內存不接受的情況出現。
-
不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變了之后必須把該變化同步回主內存。
-
不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存中。
-
一個新的變量只能在主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量,換句話說就是對一個變量實施use和store操作之前,必須先執行過了assign和load操作。
-
一個變量在同一個時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重復執行多次,多次執行lock后,只有執行相同次數的unlock操作,變量才會被解鎖。
-
如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。
-
如果一個變量事先沒有被lock操作鎖定,則不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定住的變量。
