lsp都要會的內存模型

兄弟們好,給你們帶來一篇內存模型的水文(手動滑稽)。Beginjava

先來看大綱面試

1.JMM規範

先來講JMM是什麼?數據庫

JMM(Java Memory Model):全稱Java內存模型。它定義了Java虛擬機在計算機內存中的工做方式。它是一套規範,並不真實存在。它包括三個點:原子性,可見性,有序性緩存

首先咱們來看一下它的工做原理。線程操做數據的時候須要從主內存中讀取,線程操做完數據之後進行寫回主內存。安全

可能有的兄弟要說了,爲何要搞這麼麻煩呢?我直接操做主內存中的數據不就得了,幹嗎非要複製一份再用。多線程

咱們經過一個場景來講明這個問題。架構

假設如今不存在JMM規範,咱們全部的操做都是直接在主內存中完成。併發

單線程:咱們定義了一個資源,而後在這個線程中使用這個資源,對於這個資源的修改等操做直接在主內存中完成。沒有任何問題。app

多線程:仍是同樣,咱們定義了一個資源,而後再多個線程中使用了這個資源,不一樣線程中對資源的修改所有直接操做主內存,這個時候不一樣線程之間的操做可能被相互覆蓋。優化

也就是在併發操做下會出現資源覆蓋的狀況,因而引入了Java內存模型的概念

聊完了爲何要用這個東東,咱們再來聊一下它的三個特色:

1.1 原子性

原子這個名詞最開始接觸應該是咱們在高中時期的化學吧。當時最直接的解釋就是元素最小的構成單位。固然最後又出現了夸克這樣的東東。

在其餘領域中逐漸把原子做爲了一個最小的單位,例如計算機,一個原子操做表明了這個操做不能夠被打斷即最小不可被分割。

原子性的含義就是這個:它表示這個操做不能夠被中斷

再聊原子性,在計算機領域中接觸到的應該就是數據庫的事務了吧。四大特性:原子性,一致性,隔離性,持久性。其中的原子性含義和上述相同,表明了這個事務中的操做不能夠被中斷,要麼成功要麼失敗。

1.2 可見性

不知道兄弟們看不看小說,好多玄幻小說的主角,拿到一本遠古祕籍就本身修煉,不給其餘人看(想不通,爲何不給其餘人看),此時這本祕籍相對於其餘人來講就是不可見的。而在現代圖書館,全部的書你都能看到,這個時候這些書籍就是可見的。

在計算機領域中,可見性的表現和上述的故事相似,只不過表現形式爲線程之間的數據是否可見。好比:線程A和線程B同時從主內存中讀取了一個資源,而後分別作了修改,不過線程A的操做更快一些,在線程B寫回以前就將該資源寫回了主內存(JMM規範),可是這個時候線程B不知道作了修改,此時它進行了寫回,這個時候咱們稱這個資源對於不一樣線程是不可見的。一句話說明白:不一樣線程之間不能共享資源狀態的稱爲該資源不具備可見性

那若是這個資源存在可見性,那麼當線程A將資源從新寫回主內存的時候,就會觸發一個機制,使其餘線程從新從工做內存中讀取最新的資源,而後進行操做,這個時候就表明該資源具有可見性。一句話說明白:不一樣線程之間能夠共享資源狀態稱爲該資源具備可見性

畫個圖玩一下。

1.3 有序性

這個很好理解,咱們直接上解釋。開發者編寫代碼的時候都是按照必定的順序進行編寫,而在具體執行的時候不必定會按照咱們本身寫的順序執行,JVM會進行必定的優化,對代碼進行重排,提升代碼的執行速率。

重排序類型:

  • 編譯器優化重排序。在不改變單線程執行語義的狀況下,編譯器從新梳理代碼的執行順序
  • 指令級並行重排序。現代處理器採用了指令級並行技術,將多條指令重疊執行,若是不存在數據依賴性,處理器能夠適當改變語句對應機器指令的執行順序。
  • 內存系統重排序,因爲處理器使用緩存和讀/寫緩存區,這使得加載和存儲操做看上去是亂序執行。

搞明白這些執行重排序的東東之後,咱們再看一下重排序出現的時機。

單線程執行語義:as if serial 表示在單線程狀態執行的狀況下,重排序之後的代碼和沒有進行重排序的代碼執行結果相同,即重排序不會影響代碼的正確性。

數據依賴性:表示兩條指令之間不存在數據依賴,主要表現形式爲如下的幾種狀況

只要保證了單線程執行語義和不出現上圖所述的幾種數據依賴關係就會出現指令重排序。

2. JMM內存交互

在經過JMM規範進行內存交互,依賴於下面八大內存交互操做

  • lock做用於主內存變量,把一個變量標識爲線程獨佔。其餘線程不能進行操做
  • unlock做用於主內存變量,把一個變量從線程獨佔狀態,變爲公有狀態
  • read做用於主內存變量,將主內存中的變量傳輸到工做內存中
  • load做用於工做內存變量,將read傳輸到工做內存中的變量加載到工做內存中
  • store做用於主內存變量,將工做內存中的變量加載到主內存中
  • wirte做用於工做內存變量,將store操做中的變量寫入主內存中
  • use做用於工做內存變量,當使用這個變量的時候,會經過這個指令來完成
  • assign做用於工做內存變量,當爲這個變量進行賦值的時候,會經過這個指令完成

JMM規範定義瞭如下的指令出現操做

  • readload操做必須同時出現;storewrite必須同時出現
  • 不容許線程丟棄其assign操做,若是線程對資源進行修改則必須通知主內存
  • 不容許線程將一個沒有進行assign的資源直接同步到主內存中
  • 不容許線程直接建立一個資源,全部的資源必須經過主內存常見,讀取,才能操做。也就是說在進行assignstore以前必須先進行loaduse。(待會詳細解釋
  • 一個變量在同一個時刻只能被一個線程執行一次lock操做;但能夠被同一個線程執行屢次(可重入鎖),可是lock的次數和unlock的次數應該保證相同
  • 對一個變量執行了lock操做,在unlock的時候全部的線程必須從新讀取主內存中該變量的值。(synchronized的可見性實現原理)
  • 若是一個變量沒有被lock則不能對其進行unlock操做
  • 對一個變量執行unlock操做的時候,必須將該變量同步到主內存中。

詳細和兄弟們聊聊第四條,不知道兄弟們還記不記得一個對象被存放的位置

看完上圖,是否是對上面JMM定義的內存交互規則裏的第四條,有了些許疑問。咱們再來看一遍第四條

不容許線程直接建立一個資源,全部的資源必須經過主內存常見,讀取,才能操做。也就是說在進行assignstore以前必須先進行loaduse

按照上面說的含義,全部的資源都是在主內存中建立,工做內存只能經過readload,讀取加載之後才能use,而咱們上面的一個對象的建立過程彷佛違背了這個原則。(能夠認爲,工做內存對應了棧,主內存對應了堆)

這個操做實際上是因爲JVM對常見對象的過程作的一個優化,節省因爲共享內存而形成的一系列開銷。

有兄弟要說了,你給我講這些,我在實際中怎麼進行分析呢???

沒錯,這玩意在實際中的確蠻難分析的,因此呢咱們的大JAVA提供了一個叫Happen Before的原則用來分析操做的安全性

2.1 Happen-Before

全稱:先行發生原則。

大白話:操做A先於操做B發生,則在執行操做B的時候操做A的全部修改,均可以被操做A觀察到。

3. Synchronized實現JMM規範

3.1 原子性

兄弟們應該還記得上面咱們在介紹JMM規範的時候對於原子性的相關解釋。

synchronized代碼塊能夠保證在該代碼塊中僅僅存在一個線程正在執行,因此保證了這個代碼塊中的原子性,不能被其餘線程所中斷

3.2 可見性

兄弟們還記不記得咱們在上面聊內存交互的時候提到了八個指令其中存在一個lockunlock,並且在JMM對這八個指令定義規則時候其中有一條:unlock之後其餘工做內存須要從新從主內存讀取這個變量的最新值

synchronized的底層實現就是lockunlock原子指令(能夠看JVM源碼)。

synchronized代碼塊執行完成之後,會觸發工做內存變量的刷新機制,保證變量的可見性。

畫張圖看看

3.3 有序性

咱們來看一段代碼

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沒有從根源禁止指令重排序,實際上指令重排序仍是發生了,只不過因爲加鎖了,致使其餘線程沒法進入加鎖的代碼塊,因此即便發生了指令重排序也不會對程序形成任何影響。

4. Volatile實現JMM規範

兄弟們應該都聽過這樣的面試題,聊聊你對Volatile的理解

而咱們經常使用的回答,Volatile保證可見性,有序性,可是不保證原子性

下面咱們來聊聊它是如何保證可見性和有序性。

Volatile實現可見性和有序性都是基於內存屏障實現的。下面咱們仔細聊一下內存屏障是什麼

4.1 內存屏障

咱們來看一組代碼

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屏障同時具有其餘三個屏障的效果,所以被成爲全能屏障,可是其開銷比較昂貴

4.2 可見性和有序性

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先後的可見性和有序性

JVMVolatile變量的處理。

  • 在寫volatile變量以後插入一個sfence,保證sfence以前的寫操做不會被重排序到sfence以後,同時保證其變量的可見性。
  • 在讀volatile變量以前,插入一個lfence,這樣保證了lfence以後的讀操做不會跑到lfence以前。

5. Final實現JMM規範

一個字段被聲明爲finalJVM會在初始化final變量後插入一個sfence,而類的final字段在clinit方法中初始化,由類加載過程保證其可見性,而你內存屏障保證了重排序,因此其實現了可見性和從有序性。

相關文章
相關標籤/搜索