Java虛擬機--內存模型

  內存模型同併發息息相關,熟悉內存模型將對虛擬機、多線程及線程安全問題有更深刻的瞭解。程序員

1.什麼是內存模型?編程

  給出定義以前,讓咱們先來了解一下物理計算機中的併發問題。咱們都知道,處理器運行時必然要和內存交互,並且這個I/O操做是很難消除的,但因爲計算機存儲設備和處理器的運算速度有幾個數量級的差距,因此在二者之間加入了一層讀寫速度儘量接近處理器運算速度的高速緩存,這樣處理器就不用等待緩慢的內存讀寫了。可是這樣引出了一個新的問題:緩存一致性。如圖:數組

             

  當多個處理器運算任務都涉及同一塊主內存區域時,將可能致使各自的緩存數據不一致。爲了解決一致性問題,須要各個處理器訪問緩存時都要遵照一些協議,在讀寫時要根據協議來操做。因此,內存模型能夠理解爲:在特定的操做協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象緩存

2.什麼是Java內存模型?安全

  Java內存模型即JMM(Java Memory Model),它的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節,這裏的變量指的是共享變量(如實例字段、靜態字段和構成數組對象的元素,不包括局部變量與方法參數,由於是線程私有);在併發編程中,JMM決定一個線程對共享變量的寫入什麼時候對另一個線程可見。JMM規定了全部的變量都存儲在主內存中,每一個線程都有本身的工做內存,線程對變量的全部操做都必須在工做內存中進行多線程

                   

  注:此處主內存和結構中的堆、棧等是不一樣層次上的劃分,二者基本沒有關係;若是硬要扯上關係,能夠理解爲主內存主要對應於Java堆中對象實例數據部分,而工做內存則對應於虛擬機棧中的部分區域。從更低層次上說,主內存直接對應於物理內存,爲了獲取更好的運行速度,虛擬機可能會讓工做內存優先存儲於寄存器和高速緩存中,由於程序運行時主要訪問讀寫的是工做內存。併發

3.內存間的交互操做:app

  關於主內存和工做內存之間具體的交互協議,即一個變量如何從主內存拷貝到工做內存、如何從工做內存同步回主內存之類的實現細節,Java內存模型定義了8種操做來完成:函數

    

  若是要把一個變量從主內存複製到工做內存,那就要順序地執行read和load操做,反之,就要順序地執行store和write操做。注:Java內存模型只要求上述兩個操做必須順序執行,沒有說必須連續執行。除此以外,JMM還規定了在執行上述8種基本操做時必須知足以下規則:性能

  (1).不容許read和load、store和write操做之一單獨出現。

  (2).不容許一個線程丟棄它最近的assign操做,即變量在工做內存中改變了以後,必須把該變化同步回主內存。

  (3).不容許一個線程無緣由地把數據從工做內存同步回主內存。

  (4).一個新的變量只能誕生在主內存中,即use、store以前必定要先assign、load。

  (5).一個變量同一時刻只容許一條線程對其lock,但lock操做能夠被同一線程重複屢次,而後只有執行一樣次數的unlock纔會被解鎖。

  (6).若是對一個變量執行lock操做,將會清空工做內存中此變量的值,在執行引擎使用這個變量前,需從新load或assign,初始化變量的值。

  (7).若是一個變量事先沒有被lock,那將不被容許unlock,也不容許去unlock一個被其餘線程鎖定住的變量。

  (8).對一個變量unlock前,必須把變量同步回主內存中。

  注:JMM要求這8個操做都具備原子性,但對於64位數據的long和double類型來講,容許劃分爲兩次的32位的操做來進行。多線程環境下,理論上可能有取到半個變量值的可能性,不過不用擔憂,目前商用Java虛擬機容許把這些操做視爲原子性操做,因此不用擔憂這種狀況的出現。

  總結:以上就是處理器、工做內存、主內存之間變量交互的操做;邊讀邊理解,腦海裏有一副交互圖,再來讀這幾個命令,就容易記住了。

4.JMM--併發編程模型的兩個問題

  上面咱們清楚了共享變量讀出內存和寫入內存的交互操做是怎樣的一個流程,把它想象成單線程的一條線的操做,就比較好理解。BUT,咱們想過沒有,若是是併發環境下呢?會產生什麼問題?

  問題(1):線程之間如何通訊? 問題(2).線程之間如何同步?

  在命令式編程中,線程通訊機制有兩種:共享內存消息傳遞。Java的併發採用的是共享內存模型在此模型裏,線程之間共享程序的公共狀態,經過寫-讀內存中的公共狀態進行隱式通訊,整個過程對程序員是徹底透明的;同步是指程序中用於控制不一樣線程間操做發生相對順序的機制,在共享模型裏,同步是顯式進行的,即程序員必須顯式指定某個方法或某段代碼須要在線程之間互斥執行

5.JMM--抽象結構

  從抽象角度看,JMM定義了線程和主內存之間的抽象關係:每一個線程都有一個私有的本地內存,本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的抽象概念,並不真實存在,它涵蓋了緩存、寫緩衝區、寄存器以及其餘的硬件和編譯器優化。

            

  從圖上來看,若是線程A和線程B要進行通訊,必須經歷兩個步驟:

    (1).線程A把本地內存A中更新過的共享變量刷新到主內存中。

    (2).線程B到主內存中讀取線程A已經更新過的共享變量。

  從總體上看,實質是線程A向線程B發送消息,JMM就是控制主內存與每一個線程本地內存間的交互,來提供內存可見性保證。

6.JMM--原子性、可見性、有序性

  介紹完JMM的相關操做和規則,再來總結一下JMM的特徵。JMM是圍繞着在併發中如何處理原子性可見性有序性這三個特徵來創建的。

  原子性:由Java內存模型來直接保證的原子性變量操做包括read、load、assign、use、store和write,咱們大體能夠認爲基本數據類型的訪問讀寫是具有原子性的(64位的long和double的非原子協議,知道就可,幾乎不會發生)。如須要更大範圍的原子性保證,可用同步塊--synchronized關鍵字。

  可見性:是指當一個線程修改了共享變量的值,其餘線程能夠當即得知這個修改。volatile關鍵字能夠保證多線程操做時變量的可見性,而普通變量不能。除此以外,synchronized和final也保證了可見性。

  有序性:Java程序中自然的有序性能夠總結爲一句話:「若是在本線程內觀察,全部的操做都是有序的;若是一個線程觀察另外一個線程,全部的操做都是無序的」。前半句指「線程內表現爲串行的語義」,後半句指「指令重排序現象」和「工做內存和主內存同步延遲現象」。Java提供兩個關鍵字來保證線程間操做的有序性:volatilesynchronized。前者自己就包含了指令重排序的語義;後者則是由「一個變量在同一時刻只容許一條線程對其進行lock操做」這條規則來實現,決定了持有同一個鎖的兩個同步塊只能串行執行。

7.happens-before(先行發生原則)

  Java語言中有一個「先行發生」原則,這個原則很是重要,它是判斷數據是否存在競爭、線程是否安全的主要依據。那happens-before指什麼呢?讓咱們來看一下:

  happens-before是Java內存模型中定義的兩項操做之間的偏序關係,即操做A發生在操做B以前,操做A產生的影響內被操做B觀察到,「影響」包括改變共享變變量值、發送消息、調用方法等。讓咱們看下示例:

        

  若是A操做先與操做B發生,變量j必定等於1,緣由:根據happens-before原則,A的改變能夠被B觀察到;C尚未被執行。如今來考慮C操做,A仍是先於B發生,但C出如今A和B中間,可是C和B沒有先行發生關係,那j會是多少?1仍是2?答案不肯定,由於C操做對i的改變,可能會被B觀察到,也可能不會,因此不具有線程安全性。Java內存模型存在一些自然的happens-before關係:

  程序次序規則:在一個線程內,按照代碼程序順序,書寫在前面的操做先發生於書寫在後面的操做。準確的說,應該是控制流順序而不是代碼順序,由於要考慮分支、循環等結構。

  管程鎖定規則:一個unlock操做先行發生於後面對同一個鎖的lock操做。「後面」指時間上的前後順序。

  volatile變量規則對一個volatile變量的寫操做先發生於後面對這個變量的讀操做。

  線程啓動規則:Thread對象的start()方法先行發生於此線程的每個動做。「後面」指時間上的前後順序。

  線程終止規則:線程中全部操做都先發生於此線程的終止檢測。能夠經過Thread.join()方法結束、Thread.isAlive()的返回值等檢測線程是否終止。

  線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。

  對象終結規則:一個對象初始化完成(構造函數執行完畢)先行發生於它的finalized()方法的開始。

  傳遞性:A操做先行發生於B操做,B操做先於C,則A先於C。

  那如何根據這些規則來判斷操做間是否具備順序性?對於讀寫共享變量的操做來講,就是是否線程安全?請看以下示例:

        

  這是一組普通的getter/setter方法,假設存在線程A和線程B,線程A先(時間上的前後)調用了setValue(1),而後線程B調用了同一個對象的getValue(),那線程B收的返回值是什麼?

  咱們發現沒有一個規則與之匹配,因此返回結果是不肯定的,故線程不安全。解決辦法:把getter/setter方法都定義爲synchronized方法,套用管程鎖定規則;定義爲volatile變量,因爲setter方法的修改不依賴value原值,符合只用場景由此咱們得出告終論:一個操做「時間上的先發生」不表明這個操做會先行發生。一樣,反之亦不成立(指令重排序)。一句話就是時間前後順與先行發生基本沒有太大關係,一切以先行發生原則爲準。

8.重排序

  在執行程序時,爲了提升性能編譯器處理器經常會對指令作重排序。重排序分三種類型:

      

  (1).編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序。

  (2).指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應的機器指令的執行順序。

  (3).內存系統的重排序。因爲處理器使用緩存和讀/寫緩衝區,這是的加載和存儲看上去多是在亂序中執行。

  ●數據依賴性:編譯器和處理器在重排序時,會遵照數據依賴性,不會改變存在數據依賴關係的兩個操做的執行順序。這裏只對單個處理器的指令序列和單個線程中執行的操做有效。

  ●as-if-serial:無論怎麼重排序(編譯器和處理器爲了提升並行度),(單線程)程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵照as-if-serial語義。as-if-serial語義把單線程保護了起來,便形成了一個幻覺:單線程程序按程序的順序來執行的。

  ●程序順序規則:

    double pi = 3.14;       // A                         A happens-before B   

    double r = 1.0;        // B        ====》對應三個happens-before關係:  B happens-before  C

    double area = pi * r * r;   // C                         A happens-before  C

  這裏A happens-before B,但實際執行時B卻能夠在A以前執行。JVM不要求A必定要先於B執行,僅僅要求A的操做結果對B可見,而且A的操做順序先於B操做。這裏A不須要對B可見,且重排的結果(BAC)與以前(ABC)一致,則JMM認爲這種排序並不非法,並容許。

  ●重排序對多線程的影響:在多線程中,對存在控制依賴的操做重排序,可能會改變程序的執行結果。

8.synchronized實現可見性 

        JMM關於synchronized的兩條規定:

  (1)線程解鎖前,必須把共享變量的最新值刷新到主內存中

  (2)線程加鎖時,清空工做內存中共享變量的值,從而須要共享變量時須要從主內存中讀取最新的值。

    注意:加鎖與解鎖須要是同一把鎖,線程解鎖前對共享變量的修改對其餘線程不可見。

            

            

  咱們先假設幾種狀況:執行順序爲1.1->1.2->1.3->1.4,執行結果爲6.過程:先執行write()方法,變量的改變可以及時寫入主內存,而後執行ready()方法,能夠在主內存讀取到最新的變量值;1.1->2.1->2.2->1.2,result值爲3;1.2->2.1->2.2->1.1,result值爲0

  致使共享變量在線程間不可見的緣由:(1)線程的交叉執行  (2)重排序結合線程交叉執行  (3)共享變量更新後的值沒有在工做內存與主內存及時更新  

  解決辦法:在保證寫線程先執行的前提下,用write、ready方法用synchronized修飾。首先阻止了線程的交叉執行,其次單線程的重排序不影響結果,最後對變量的改變可見。

9.volatile實現可見性

  volatile關鍵字:保證變量的可見性,不能保證變量複合操做的原子性。

  如何保證可見性?深刻來講:經過加入內存屏障和禁止重排序優化來實現的。

  ●對volatile變量執行寫操做時,會在寫操做後加入一條store屏障指令,它會把寫緩存強制刷新到主內存中去,這樣主內存中就是變量的最新值,同時防止處理器把volatile前面的變量重排序到寫變量以後。

  對volatile變量執行讀操做時,會在寫操做後加入一條load屏障指令,它會使緩存區的變量失效。

相關文章
相關標籤/搜索