兄弟們好,給你們帶來一篇內存模型的水文(手動滑稽)。Beginjava
先來看大綱面試
先來講JMM
是什麼?數據庫
JMM(Java Memory Model)
:全稱Java
內存模型。它定義了Java
虛擬機在計算機內存中的工做方式。它是一套規範,並不真實存在。它包括三個點:原子性,可見性,有序性緩存
首先咱們來看一下它的工做原理。線程操做數據的時候須要從主內存中讀取,線程操做完數據之後進行寫回主內存。安全
可能有的兄弟要說了,爲何要搞這麼麻煩呢?我直接操做主內存中的數據不就得了,幹嗎非要複製一份再用。多線程
咱們經過一個場景來講明這個問題。架構
假設如今不存在JMM
規範,咱們全部的操做都是直接在主內存中完成。併發
單線程:咱們定義了一個資源,而後在這個線程中使用這個資源,對於這個資源的修改等操做直接在主內存中完成。沒有任何問題。app
多線程:仍是同樣,咱們定義了一個資源,而後再多個線程中使用了這個資源,不一樣線程中對資源的修改所有直接操做主內存,這個時候不一樣線程之間的操做可能被相互覆蓋。優化
也就是在併發操做下會出現資源覆蓋的狀況,因而引入了Java內存模型的概念
聊完了爲何要用這個東東,咱們再來聊一下它的三個特色:
原子這個名詞最開始接觸應該是咱們在高中時期的化學吧。當時最直接的解釋就是元素最小的構成單位。固然最後又出現了夸克這樣的東東。
在其餘領域中逐漸把原子做爲了一個最小的單位,例如計算機,一個原子操做表明了這個操做不能夠被打斷即最小不可被分割。
原子性的含義就是這個:它表示這個操做不能夠被中斷
再聊原子性,在計算機領域中接觸到的應該就是數據庫的事務了吧。四大特性:原子性,一致性,隔離性,持久性。其中的原子性含義和上述相同,表明了這個事務中的操做不能夠被中斷,要麼成功要麼失敗。
不知道兄弟們看不看小說,好多玄幻小說的主角,拿到一本遠古祕籍就本身修煉,不給其餘人看(想不通,爲何不給其餘人看),此時這本祕籍相對於其餘人來講就是不可見的。而在現代圖書館,全部的書你都能看到,這個時候這些書籍就是可見的。
在計算機領域中,可見性的表現和上述的故事相似,只不過表現形式爲線程之間的數據是否可見。好比:線程A和線程B同時從主內存中讀取了一個資源,而後分別作了修改,不過線程A的操做更快一些,在線程B寫回以前就將該資源寫回了主內存(JMM規範),可是這個時候線程B不知道作了修改,此時它進行了寫回,這個時候咱們稱這個資源對於不一樣線程是不可見的。一句話說明白:不一樣線程之間不能共享資源狀態的稱爲該資源不具備可見性
那若是這個資源存在可見性,那麼當線程A將資源從新寫回主內存的時候,就會觸發一個機制,使其餘線程從新從工做內存中讀取最新的資源,而後進行操做,這個時候就表明該資源具有可見性。一句話說明白:不一樣線程之間能夠共享資源狀態稱爲該資源具備可見性
畫個圖玩一下。
這個很好理解,咱們直接上解釋。開發者編寫代碼的時候都是按照必定的順序進行編寫,而在具體執行的時候不必定會按照咱們本身寫的順序執行,JVM
會進行必定的優化,對代碼進行重排,提升代碼的執行速率。
重排序類型:
搞明白這些執行重排序的東東之後,咱們再看一下重排序出現的時機。
單線程執行語義:as if serial
表示在單線程狀態執行的狀況下,重排序之後的代碼和沒有進行重排序的代碼執行結果相同,即重排序不會影響代碼的正確性。
數據依賴性:表示兩條指令之間不存在數據依賴,主要表現形式爲如下的幾種狀況
只要保證了單線程執行語義和不出現上圖所述的幾種數據依賴關係就會出現指令重排序。
在經過JMM
規範進行內存交互,依賴於下面八大內存交互操做
lock
,做用於主內存變量,把一個變量標識爲線程獨佔。其餘線程不能進行操做unlock
,做用於主內存變量,把一個變量從線程獨佔狀態,變爲公有狀態read
,做用於主內存變量,將主內存中的變量傳輸到工做內存中load
,做用於工做內存變量,將read
傳輸到工做內存中的變量加載到工做內存中store
,做用於主內存變量,將工做內存中的變量加載到主內存中wirte
,做用於工做內存變量,將store
操做中的變量寫入主內存中use
,做用於工做內存變量,當使用這個變量的時候,會經過這個指令來完成assign
,做用於工做內存變量,當爲這個變量進行賦值的時候,會經過這個指令完成JMM
規範定義瞭如下的指令出現操做
read
和load
操做必須同時出現;store
和write
必須同時出現assign
操做,若是線程對資源進行修改則必須通知主內存assign
的資源直接同步到主內存中assign
和store
以前必須先進行load
和use
。(待會詳細解釋)lock
操做;但能夠被同一個線程執行屢次(可重入鎖),可是lock
的次數和unlock
的次數應該保證相同lock
操做,在unlock
的時候全部的線程必須從新讀取主內存中該變量的值。(synchronized
的可見性實現原理)lock
則不能對其進行unlock
操做unlock
操做的時候,必須將該變量同步到主內存中。詳細和兄弟們聊聊第四條,不知道兄弟們還記不記得一個對象被存放的位置
看完上圖,是否是對上面JMM
定義的內存交互規則裏的第四條,有了些許疑問。咱們再來看一遍第四條
不容許線程直接建立一個資源,全部的資源必須經過主內存常見,讀取,才能操做。也就是說在進行assign
和store
以前必須先進行load
和use
按照上面說的含義,全部的資源都是在主內存中建立,工做內存只能經過read
和load
,讀取加載之後才能use
,而咱們上面的一個對象的建立過程彷佛違背了這個原則。(能夠認爲,工做內存對應了棧,主內存對應了堆)
這個操做實際上是因爲JVM
對常見對象的過程作的一個優化,節省因爲共享內存而形成的一系列開銷。
有兄弟要說了,你給我講這些,我在實際中怎麼進行分析呢???
沒錯,這玩意在實際中的確蠻難分析的,因此呢咱們的大JAVA
提供了一個叫Happen Before
的原則用來分析操做的安全性
全稱:先行發生原則。
大白話:操做A先於操做B發生,則在執行操做B的時候操做A的全部修改,均可以被操做A觀察到。
兄弟們應該還記得上面咱們在介紹JMM
規範的時候對於原子性的相關解釋。
synchronized
代碼塊能夠保證在該代碼塊中僅僅存在一個線程正在執行,因此保證了這個代碼塊中的原子性,不能被其餘線程所中斷
兄弟們還記不記得咱們在上面聊內存交互的時候提到了八個指令其中存在一個lock
和unlock
,並且在JMM
對這八個指令定義規則時候其中有一條:unlock
之後其餘工做內存須要從新從主內存讀取這個變量的最新值
而synchronized
的底層實現就是lock
和unlock
原子指令(能夠看JVM
源碼)。
當synchronized
代碼塊執行完成之後,會觸發工做內存變量的刷新機制,保證變量的可見性。
畫張圖看看
咱們來看一段代碼
private int num = 0 ; private boolean flag = false ; public int test01(){ synchronized (this){ if(!flag){ num = 2 ; } } return num ; } public void test02(){ synchronized (this){ num = 4 ; flag = true ; } }
能夠發現synchronized
沒有從根源禁止指令重排序,實際上指令重排序仍是發生了,只不過因爲加鎖了,致使其餘線程沒法進入加鎖的代碼塊,因此即便發生了指令重排序也不會對程序形成任何影響。
兄弟們應該都聽過這樣的面試題,聊聊你對Volatile的理解
而咱們經常使用的回答,Volatile保證可見性,有序性,可是不保證原子性
下面咱們來聊聊它是如何保證可見性和有序性。
Volatile
實現可見性和有序性都是基於內存屏障實現的。下面咱們仔細聊一下內存屏障是什麼
咱們來看一組代碼
x = x + y ; z = 3 ;
這兩行代碼的執行順序不是咱們開發者能夠控制的,計算機內部會進行編譯優化或者運行優化,也就是說,第二行的代碼可能優於第一行代碼執行。而咱們想要保證代碼的執行順序,每每須要採起一系列措施,如硬件措施或者軟件措施等。
而內存屏障就是在硬件之上,操做系統和JVM
之下的對併發作的最後的一層封裝。
咱們先來聊一下CPU
層面的併發處理方案。
兄弟們應該還記得咱們以前將Synchronized
的時候涉及到的CPU
的架構
上圖就是CPU
的架構圖,一個CPU
兩個核心,單獨的一級和二級緩存,三級緩存公用。而在併發操做的時候就會出現數據衝突的問題,也就是緩存一致性的問題。而解決這種問題,CPU
廠商提供了一個解決方法:MESI
協議。
MESI
表明了四種狀態,下面是對這四種狀態的解釋
M
(修改,Modified
):本地處理器已經修改緩存行,便是髒行,它的內容與內存中的內容不同,而且此cache
只有本地一個拷貝E
(專有,Exclusive
):緩存行內容和內存中的同樣,並且其餘處理器都沒有這行數據。S
(共享,Shared
):緩存行規內容和內存中的同樣,有可能其餘處理器也存在該緩存行的拷貝I
(無效,Invalid
):緩存行失效,不能使用。咱們簡單的聊一下這個協議在實際中的應用
Core1
修改值之後,會發送一個信號給其餘正在對該值進行操做的核,改變其餘核中值得狀態Core1
修改多少次值,就會發送多少次信號給其餘正在進行操做的核。(發信號的時候會鎖總線)MESI
協議保證了在CPU
中緩存的可見性。但在內存中卻沒法保證其可見性等。因此這個時候就須要內存屏障來解決這個事情了。
其實咱們能夠本身思考一下,內存屏障如何作:無非就是在須要保證JMM
規範的語句中加入一個塊,讓CPU或者編譯器不對該塊內容進行重排序,因此呢,組成這個塊的就是兩個指令
內存屏障的兩大指令
load
將內存中的數據拷貝處處理器中store
將處理器中緩存的數據刷回內存中咱們看一個工做圖
這兩個指令組合起來就造成了四種屏障類型
屏障類型 | 指令說明 | 說明 |
---|---|---|
LoadLoadBarriers | Load1; LoadLoad; Load2 |
確保Load1數據的裝載優先於Load2 |
StoreStoreBarriers | Store1; StoreStore store2 |
確保Store1刷新數據到內存(此時數據對其餘處理器可見)的操做先於store2的刷新數據到內存中 |
LoadStoreBarriers | Load1; LoadStore; Store2 |
確保Load1的數據狀態先於Store2的數據刷回內存種 |
StoreLoadBarries | Store1; StoreLoad; Load2 |
確保store1的數據刷回內存的操做先於Load2 的數據裝載 |
其中的StoreLoad Barriers
屏障同時具有其餘三個屏障的效果,所以被成爲全能屏障,可是其開銷比較昂貴
Volatile
是如何保證可見性和有序性的?
咱們以X86
系統架構來講明,
對於X86
系統而言,它僅僅實現了三種內存屏障
Store Barrier
sfence
指令實現了Store barrier
,至關於咱們上面提到的StoreStore Barriers
。它強制把sfence
指令以前的store
指令所有在其以前執行。即禁止sfence
先後的store
指令跨越sfence
執行,而且全部在sfence
以前的內存更新都是可見的
Load Barrier
lfence
指令實現Load Barrier
,至關於咱們前面提到的loadLoad Barriers
,它強制全部在lfence
指令以後的load
所有在lfence
以後執行。配合StoreBarrier
使用,使得sfence
以前的內存更新對與lfencec
以後的Load操做都是可見的
Full Barrier
mfence
指令實現了Full Barrier
至關於StoreLoad Barriers
。
它強制全部的mfencec
指令以前的store/load
指令都在該指令執行以前執行,保證了mfence
先後的可見性和有序性
JVM
對Volatile
變量的處理。
volatile
變量以後插入一個sfence
,保證sfence
以前的寫操做不會被重排序到sfence
以後,同時保證其變量的可見性。volatile
變量以前,插入一個lfence
,這樣保證了lfence
以後的讀操做不會跑到lfence
以前。一個字段被聲明爲final
,JVM
會在初始化final
變量後插入一個sfence
,而類的final
字段在clinit
方法中初始化,由類加載過程保證其可見性,而你內存屏障保證了重排序,因此其實現了可見性和從有序性。