標籤:tac 元素 就是 方式 html 原子操作 blog 髒資料 靜態變數
Java記憶體模型JMMjava記憶體模型定義
上一遍文章我們講到了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一個被其他線程鎖定住的變數。
回顧1.Java記憶體模型是一個規範,他規定了不同線程如何以及何時可以看到其他線程寫入共用變數的值以及如何在必要時同步對共用變數的訪問。2.java記憶體模型要求,調用棧和本地變數儲存線上程棧上,對象存放在堆上。線程之間的通訊必須要經過主記憶體。3.定義了同步的八個操作,以及使用這八個操作需要遵守的規則。
死磕並發之Java記憶體模型(Java Memory Model)