【Todo】【轉載】深刻理解Java內存模型

提綱挈領地說一下Java內存模型:html

什麼是Java內存模型

Java內存模型定義了一種多線程訪問Java內存的規範。Java內存模型要完整講不是這裏幾句話能說清楚的,我簡單總結一下Java內存模型的幾部份內容:java

(1)Java內存模型將內存分爲了 主內存和工做內存 。類的狀態,也就是類之間共享的變量,是存儲在主內存中的,每次Java線程用到這些主內存中的變量的時候,會讀一次主內存中的變量,並讓這些內存在本身的工做內存中有一份拷貝,運行本身線程代碼的時候,用到這些變量,操做的都是本身工做內存中的那一份。在線程代碼執行完畢以後,會將最新的值更新到主內存中去程序員

(2)定義了幾個原子操做,用於操做主內存和工做內存中的變量編程

(3)定義了volatile變量的使用規則數組

(4)happens-before,即先行發生原則,定義了操做A必然先行發生於操做B的一些規則,好比在同一個線程內控制流前面的代碼必定先行發生於控制流後面的代碼、一個釋放鎖unlock的動做必定先行發生於後面對於同一個鎖進行鎖定lock的動做等等。緩存

這一句沒懂:(只要符合這些規則,則不須要額外作同步措施,若是某段代碼不符合全部的happens-before規則,則這段代碼必定是線程非安全的)安全

 

 

參考Infoq上的這篇文章:Link數據結構

《深刻理解Java內存模型(一)——基礎》多線程

 

併發編程模型的分類

在併發編程中,咱們須要處理兩個關鍵問題:線程之間如何通訊及線程之間如何同步(這裏的線程是指併發執行的活動實體)。通訊是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的通訊機制有兩種:共享內存消息傳遞併發

在共享內存的併發模型裏,線程之間共享程序的公共狀態,線程之間經過寫-讀內存中的公共狀態來隱式進行通訊。

在消息傳遞的併發模型裏,線程之間沒有公共狀態,線程之間必須經過明確的發送消息來顯式進行通訊。

 

同步是指程序用於控制不一樣線程之間操做發生相對順序的機制。

在共享內存併發模型裏,同步是顯式進行的。程序員必須顯式指定某個方法或某段代碼須要在線程之間互斥執行。

在消息傳遞的併發模型裏,因爲消息的發送必須在消息的接收以前,所以同步是隱式進行的。

 

Java的併發採用的是共享內存模型,Java線程之間的通訊老是隱式進行,整個通訊過程對程序員徹底透明。若是編寫多線程程序的Java程序員不理解隱式進行的線程之間通訊的工做機制,極可能會遇到各類奇怪的內存可見性問題。

原生Java線程之間只能經過共享內存(同一個虛擬機內)來通訊,固然你能夠經過本身實現,使得線程看起來能夠經過消息通訊(好比Scala的Actor)
能夠經過消息傳遞,但Actor自己和線程是有很大不一樣,不過看起來具有了一些線程功能 。

Java內存模型的抽象

在java中,全部實例域、靜態域和數組元素存儲在堆內存中,堆內存在線程之間共享(本文使用「共享變量」這個術語代指實例域,靜態域和數組元素)。

局部變量(Local variables),方法定義參數(java語言規範稱之爲formal method parameters)和異常處理器參數(exception handler parameters)不會在線程之間共享,它們不會有內存可見性問題,也不受內存模型的影響。

 

Java線程之間的通訊由Java內存模型(本文簡稱爲JMM)控制,JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(main memory)中,每一個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化。Java內存模型的抽象示意圖以下:

 

從上圖來看,線程A與線程B之間如要通訊的話,必需要經歷下面2個步驟:

  1. 首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。
  2. 而後,線程B到主內存中去讀取線程A以前已更新過的共享變量。

下面經過示意圖來講明這兩個步驟

從總體來看,這兩個步驟實質上是線程A在向線程B發送消息,並且這個通訊過程必需要通過主內存。JMM經過控制主內存與每一個線程的本地內存之間的交互,來爲java程序員提供內存可見性保證。

 

重排序

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

  1. 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序。
  2. 指令級並行的重排序。現代處理器採用了指令級並行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。
  3. 內存系統的重排序。因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。

從java源代碼到最終實際執行的指令序列,會分別經歷下面三種重排序:

上述的1屬於編譯器重排序,2和3屬於處理器重排序。這些重排序均可能會致使多線程程序出現內存可見性問題。對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是全部的編譯器重排序都要禁止)。對於處理器重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定類型的內存屏障(memory barriers,intel稱之爲memory fence)指令,經過內存屏障指令來禁止特定類型的處理器重排序(不是全部的處理器重排序都要禁止)。

JMM屬於語言級的內存模型,它確保在不一樣的編譯器和不一樣的處理器平臺之上,經過禁止特定類型的編譯器重排序和處理器重排序,爲程序員提供一致的內存可見性保證。

 

處理器重排序與內存屏障指令

現代的處理器使用寫緩衝區來臨時保存向內存寫入的數據。

寫緩衝區能夠保證指令流水線持續運行,它能夠避免因爲處理器停頓下來等待向內存寫入數據而產生的延遲。
同時,經過以批處理的方式刷新寫緩衝區,以及合併寫緩衝區中對同一內存地址的屢次寫,能夠減小對內存總線的佔用。

雖然寫緩衝區有這麼多好處,但每一個處理器上的寫緩衝區,僅僅對它所在的處理器可見。這個特性會對內存操做的執行順序產生重要的影響:處理器對內存的讀/寫操做的執行順序,不必定與內存實際發生的讀/寫操做順序一致!

爲了具體說明,請看下面示例:

 

Processor A Processor B
a = 1; //A1
x = b; //A2
b = 2; //B1
y = a; //B2
初始狀態:a = b = 0
處理器容許執行後獲得結果:x = y = 0

假設處理器A和處理器B按程序的順序並行執行內存訪問,最終卻可能獲得x = y = 0的結果。具體的緣由以下圖所示:

這裏處理器A和處理器B能夠同時把共享變量寫入本身的寫緩衝區(A1,B1),而後從內存中讀取另外一個共享變量(A2,B2)
最後才把本身寫緩存區中保存的髒數據刷新到內存中(A3,B3)。當以這種時序執行時,程序就能夠獲得x = y = 0的結果。

從內存操做實際發生的順序來看,直處處理器A執行A3來刷新本身的寫緩存區,寫操做A1纔算真正執行了。
雖然處理器A執行內存操做的順序爲:A1->A2,但內存操做實際發生的順序倒是:A2->A1。此時,處理器A的內存操做順序被重排序了。

這裏的關鍵是,因爲寫緩衝區僅對本身的處理器可見,它會致使處理器執行內存操做的順序可能會與內存實際的操做執行順序不一致。因爲現代的處理器都會使用寫緩衝區,所以現代的處理器都會容許對寫-讀操作重排序。

下面是常見處理器容許的重排序類型的列表:

  Load-Load Load-Store Store-Store Store-Load 數據依賴
sparc-TSO N N N Y N
x86 N N N Y N
ia64 Y Y Y Y N
PowerPC Y Y Y Y N

上表單元格中的「N」表示處理器不容許兩個操做重排序,「Y」表示容許重排序。

從上表咱們能夠看出:常見的處理器都容許Store-Load重排序;常見的處理器都不容許對存在數據依賴的操做作重排序。
sparc-TSO和x86擁有相對較強的處理器內存模型,它們僅容許對寫-讀操做作重排序(由於它們都使用了寫緩衝區)。 ※注1:sparc-TSO是指以TSO(Total Store Order)內存模型運行時,sparc處理器的特性。 ※注2:上表中的x86包括x64及AMD64。 ※注3:因爲ARM處理器的內存模型與PowerPC處理器的內存模型很是相似,本文將忽略它。 ※注4:數據依賴性後文會專門說明。

 

爲了保證內存可見性,java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。JMM把內存屏障指令分爲下列四類:

 

屏障類型 指令示例 說明
LoadLoad Barriers Load1; LoadLoad; Load2 確保Load1數據的裝載,以前於Load2及全部後續裝載指令的裝載。
StoreStore Barriers Store1; StoreStore; Store2 確保Store1數據對其餘處理器可見(刷新到內存),以前於Store2及全部後續存儲指令的存儲。
LoadStore Barriers Load1; LoadStore; Store2 確保Load1數據裝載,以前於Store2及全部後續的存儲指令刷新到內存。
StoreLoad Barriers Store1; StoreLoad; Load2 確保Store1數據對其餘處理器變得可見(指刷新到內存),以前於Load2及全部後續裝載指令的裝載。StoreLoad Barriers會使該屏障以前的全部內存訪問指令(存儲和裝載指令)完成以後,才執行該屏障以後的內存訪問指令。

StoreLoad Barriers是一個「全能型」的屏障,它同時具備其餘三個屏障的效果。現代的多處理器大都支持該屏障(其餘類型的屏障不必定被全部處理器支持)。執行該屏障開銷會很昂貴,由於當前處理器一般要把寫緩衝區中的數據所有刷新到內存中(buffer fully flush)。

 

happens-before

從JDK5開始,java使用新的JSR -133內存模型(本文除非特別說明,針對的都是JSR- 133內存模型)。JSR-133提出了happens-before的概念,經過這個概念來闡述操做之間的內存可見性。若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必須存在happens-before關係。這裏提到的兩個操做既能夠是在一個線程以內,也能夠是在不一樣線程之間。

與程序員密切相關的happens-before規則以下:

  • 程序順序規則:一個線程中的每一個操做,happens- before 於該線程中的任意後續操做。
  • 監視器鎖規則:對一個監視器鎖的解鎖,happens- before 於隨後對這個監視器鎖的加鎖。
  • volatile變量規則:對一個volatile域的寫,happens- before 於任意後續對這個volatile域的讀。
  • 傳遞性:若是A happens- before B,且B happens- before C,那麼A happens- before C。

注意,兩個操做之間具備happens-before關係,並不意味着前一個操做必需要在後一個操做以前執行!happens-before僅僅要求前一個操做(執行的結果)對後一個操做可見,且前一個操做按順序排在第二個操做以前(the first is visible to and ordered before the second)。happens- before的定義很微妙,後文會具體說明happens-before爲何要這麼定義。

 

happens-before與JMM的關係以下圖所示:

如上圖所示,一個happens-before規則一般對應於多個編譯器重排序規則和處理器重排序規則。對於java程序員來講,happens-before規則簡單易懂,它避免程序員爲了理解JMM提供的內存可見性保證而去學習複雜的重排序規則以及這些規則的具體實現。

 

注:以上happens-before規則,指的就是程序員不用去管底層的重排序規則,哪些被禁止了哪些還生效,只須要知道上面4類規則一直是生效的,而且應用於代碼中,就能夠了。

 

繼續。以上這個文章系列的電子版已經下載:

/Users/baidu/Documents/Data/Interview/Java/think_deep_in_java_mem_model.pdf

可是電子版複製粘貼不方便,因此仍是在Infoq原文中看。

 

開始看第二篇:http://www.infoq.com/cn/articles/java-memory-model-2    (完整的系列,點做者名字就能看到)

數據依賴性

若是兩個操做訪問同一個變量,且這兩個操做中有一個爲寫操做,此時這兩個操做之間就存在數據依賴性。數據依賴分下列三種類型:

 

名稱 代碼示例 說明
寫後讀 a = 1;b = a; 寫一個變量以後,再讀這個位置。
寫後寫 a = 1;a = 2; 寫一個變量以後,再寫這個變量。
讀後寫 a = b;b = 1; 讀一個變量以後,再寫這個變量。

上面三種狀況,只要重排序兩個操做的執行順序,程序的執行結果將會被改變。

前面提到過,編譯器和處理器可能會對操做作重排序。編譯器和處理器在重排序時,會遵照數據依賴性,編譯器和處理器不會改變存在數據依賴關係的兩個操做的執行順序。

注意,這裏所說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操做,不一樣處理器之間和不一樣線程之間的數據依賴性不被編譯器和處理器考慮。

as-if-serial語義

as-if-serial語義的意思指:無論怎麼重排序(編譯器和處理器爲了提升並行度),(單線程)程序的執行結果不能被改變。編譯器,runtime 和處理器都必須遵照as-if-serial語義。

double pi  = 3.14;    //A
double r   = 1.0;     //B
double area = pi * r * r; //C

下圖是該程序的兩種執行順序:

程序順序規則

根據happens- before的程序順序規則,上面計算圓的面積的示例代碼存在三個happens- before關係:

  1. A happens- before B;
  2. B happens- before C;
  3. A happens- before C;

這裏A happens- before B,但實際執行時B卻能夠排在A以前執行(看上面的重排序後的執行順序)。在第一章提到過,若是A happens- before B,JMM並不要求A必定要在B以前執行。JMM僅僅要求前一個操做(執行的結果)對後一個操做可見,且前一個操做按順序排在第二個操做以前。這裏操做A的執行結果不須要對操做B可見;並且重排序操做A和操做B後的執行結果,與操做A和操做B按happens- before順序執行的結果一致。在這種狀況下,JMM會認爲這種重排序並不非法(not illegal),JMM容許這種重排序。

在計算機中,軟件技術和硬件技術有一個共同的目標:在不改變程序執行結果的前提下,儘量的開發並行度。編譯器和處理器聽從這一目標,從happens- before的定義咱們能夠看出,JMM一樣聽從這一目標。

 

 

重排序對多線程的影響(這個例子挺重要,可以加深對重排序的理解)

如今讓咱們來看看,重排序是否會改變多線程程序的執行結果。請看下面的示例代碼:

class ReorderExample {
int a = 0;
boolean flag = false;

// 線程A執行
public void writer() { a = 1; //1 flag = true; //2 }
// 線程B執行 Public
void reader() { if (flag) { //3 int i = a * a; //4 …… } } }

單線程程序中,對存在控制依賴的操做重排序,不會改變執行結果(這也是as-if-serial語義容許對存在控制依賴的操做作重排序的緣由);

但在多線程程序中,線程A和B分別執行上面這兩個函數,對存在控制依賴的操做重排序,可能會改變程序的執行結果。

 

因爲操做1和操做2沒有數據依賴關係,編譯器和處理器能夠對這兩個操做重排序;

一樣,操做3和操做4沒有數據依賴關係(只有控制依賴關係,會用預存關係,下面有詳細介紹),編譯器和處理器也能夠對這兩個操做重排序。

 

讓咱們先來看看,當操做1和操做2重排序時,可能會產生什麼效果?請看下面的程序執行時序圖:

如上圖所示,操做1和操做2作了重排序。程序執行時,線程A首先寫標記變量flag,隨後線程B讀這個變量。因爲條件判斷爲真,線程B將讀取變量a。此時,變量a還根本沒有被線程A寫入,在這裏多線程程序的語義被重排序破壞了!

 

下面再讓咱們看看,當操做3和操做4重排序時會產生什麼效果(藉助這個重排序,能夠順便說明控制依賴性)。下面是操做3和操做4重排序後,程序的執行時序圖:

在程序中,操做3和操做4存在控制依賴關係。當代碼中存在控制依賴性時,會影響指令序列執行的並行度。爲此,編譯器和處理器會採用猜想(Speculation)執行來克服控制相關性對並行度的影響。以處理器的猜想執行爲例,執行線程B的處理器能夠提早讀取並計算a*a,而後把計算結果臨時保存到一個名爲重排序緩衝(reorder buffer ROB)的硬件緩存中。當接下來操做3的條件判斷爲真時,就把該計算結果寫入變量i中。

從圖中咱們能夠看出,猜想執行實質上對操做3和4作了重排序。重排序在這裏破壞了多線程程序的語義!

 

下一篇《深刻理解Java內存模型(三)——順序一致性》(全系列能夠點做者名進入)

http://www.infoq.com/cn/articles/java-memory-model-3

數據競爭與順序一致性保證

順序一致性內存模型有兩大特性:

  • 一個線程中的全部操做必須按照程序的順序來執行。
  • (無論程序是否同步)全部線程都只能看到一個單一的操做執行順序。在順序一致性內存模型中,每一個操做都必須原子執行且馬上對全部線程可見。

可是,在JMM中就沒有這個保證。未同步程序在JMM中不但總體的執行順序是無序的,並且全部線程看到的操做執行順序也可能不一致。好比,在當前線程把寫過的數據緩存在本地內存中,且尚未刷新到主內存以前,這個寫操做僅對當前線程可見;從其餘線程的角度來觀察,會認爲這個寫操做根本尚未被當前線程執行。只有當前線程把本地內存中寫過的數據刷新到主內存以後,這個寫操做才能對其餘線程可見。在這種狀況下,當前線程和其它線程看到的操做執行順序將不一致。

 

這一篇不太好懂。先看下面一篇吧。

http://www.infoq.com/cn/articles/java-memory-model-4

 

volatile的特性

當咱們聲明共享變量爲volatile後,對這個變量的讀/寫將會很特別。理解volatile特性的一個好方法是:把對volatile變量的單個讀/寫,當作是使用同一個監視器鎖對這些單個讀/寫操做作了同步。下面咱們經過具體的示例來講明,請看下面的示例代碼:

class VolatileFeaturesExample {
    volatile long vl = 0L;  //使用volatile聲明64位的long型變量

    public void set(long l) {
        vl = l;   //單個volatile變量的寫
    }

    public void getAndIncrement () {
        vl++;    //複合(多個)volatile變量的讀/寫
    }


    public long get() {
        return vl;   //單個volatile變量的讀
    }
}

 

假設有多個線程分別調用上面程序的三個方法,這個程序在語意上和下面程序等價:

class VolatileFeaturesExample {
    long vl = 0L;               // 64位的long型普通變量

    public synchronized void set(long l) {     //對單個的普通 變量的寫用同一個監視器同步
        vl = l;
    }

    public void getAndIncrement () { //普通方法調用
        long temp = get();           //調用已同步的讀方法
        temp += 1L;                  //普通寫操做
        set(temp);                   //調用已同步的寫方法
    }
    public synchronized long get() { 
    //對單個的普通變量的讀用同一個監視器同步
        return vl;
    }
}

如上面示例程序所示,對一個volatile變量的單個讀/寫操做,與對一個普通變量的讀/寫操做使用同一個監視器鎖來同步,它們之間的執行效果相同。

監視器鎖的happens-before規則保證釋放監視器和獲取監視器的兩個線程之間的內存可見性,這意味着對一個volatile變量的讀,老是能看到(任意線程)對這個volatile變量最後的寫入。

 

監視器鎖的語義決定了臨界區代碼的執行具備原子性。這意味着即便是64位的long型和double型變量,只要它是volatile變量,對該變量的讀寫就將具備原子性。若是是多個volatile操做或相似於volatile++這種複合操做,這些操做總體上不具備原子性

簡而言之,volatile變量自身具備下列特性:

  • 可見性:對一個volatile變量的讀,老是能看到(任意線程)對這個volatile變量最後的寫入。
  • 原子性:對任意單個volatile變量的讀/寫具備原子性,但相似於volatile++這種複合操做不具備原子性。

 

volatile寫-讀創建的happens before關係

上面講的是volatile變量自身的特性,對程序員來講,volatile對線程的內存可見性的影響比volatile自身的特性更爲重要,也更須要咱們去關注。

從JSR-133開始,volatile變量的寫-讀能夠實現線程之間的通訊。

從內存語義的角度來講,volatile與監視器鎖有相同的效果:volatile寫和監視器的釋放有相同的內存語義;volatile讀與監視器的獲取有相同的內存語義。

請看下面使用volatile變量的示例代碼:

class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;                   //1
        flag = true;               //2
    }

    public void reader() {
        if (flag) {                //3
            int i =  a;           //4
            ……
        }
    }
}

假設線程A執行writer()方法以後,線程B執行reader()方法。根據happens before規則,這個過程創建的happens before 關係能夠分爲兩類:

  1. 根據程序次序規則,1 happens before 2; 3 happens before 4。
  2. 根據volatile規則,2 happens before 3。
  3. 根據happens before 的傳遞性規則,1 happens before 4。

上述happens before 關係的圖形化表現形式以下:

在上圖中,每個箭頭連接的兩個節點,表明了一個happens before 關係。黑色箭頭表示程序順序規則;橙色箭頭表示volatile規則;藍色箭頭表示組合這些規則後提供的happens before保證。

這裏A線程寫一個volatile變量後,B線程讀同一個volatile變量。A線程在寫volatile變量以前全部可見的共享變量,在B線程讀同一個volatile變量後,將當即變得對B線程可見。

 (注意:happens-before不必定被JMM實現,可是volatile的happens-before被實現了,見後文)

 

volatile寫-讀的內存語義

volatile寫的內存語義以下:

  • 當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存。

以上面示例程序VolatileExample爲例,假設線程A首先執行writer()方法,隨後線程B執行reader()方法,初始時兩個線程的本地內存中的flag和a都是初始狀態。下圖是線程A執行volatile寫後,共享變量的狀態示意圖:

如上圖所示,線程A在寫flag變量後,本地內存A中被線程A更新過的兩個共享變量的值被刷新到主內存中。此時,本地內存A和主內存中的共享變量的值是一致的。

volatile讀的內存語義以下:

  • 當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。

下面是線程B讀同一個volatile變量後,共享變量的狀態示意圖:

如上圖所示,在讀flag變量後,本地內存B已經被置爲無效。此時,線程B必須從主內存中讀取共享變量。線程B的讀取操做將致使本地內存B與主內存中的共享變量的值也變成一致的了。

若是咱們把volatile寫和volatile讀這兩個步驟綜合起來看的話,在讀線程B讀一個volatile變量後,寫線程A在寫這個volatile變量以前全部可見的共享變量的值都將當即變得對讀線程B可見。

下面對volatile寫和volatile讀的內存語義作個總結:

  • 線程A寫一個volatile變量,實質上是線程A向接下來將要讀這個volatile變量的某個線程發出了(其對共享變量所在修改的)消息。
  • 線程B讀一個volatile變量,實質上是線程B接收了以前某個線程發出的(在寫這個volatile變量以前對共享變量所作修改的)消息。
  • 線程A寫一個volatile變量,隨後線程B讀這個volatile變量,這個過程實質上是線程A經過主內存向線程B發送消息。

 

volatile內存語義的實現

下面,讓咱們來看看JMM如何實現volatile寫/讀的內存語義。

前文咱們提到太重排序分爲編譯器重排序和處理器重排序。爲了實現volatile內存語義,JMM會分別限制這兩種類型的重排序類型。下面是JMM針對編譯器制定的volatile重排序規則表:

是否能重排序 第二個操做
第一個操做 普通讀/寫 volatile讀 volatile寫
普通讀/寫     NO
volatile讀 NO NO NO
volatile寫   NO NO

舉例來講,第三行最後一個單元格的意思是:在程序順序中,當第一個操做爲普通變量的讀或寫時,若是第二個操做爲volatile寫,則編譯器不能重排序這兩個操做。

從上表咱們能夠看出:

  • 當第二個操做是volatile寫時,無論第一個操做是什麼,都不能重排序。這個規則確保volatile寫以前的操做不會被編譯器重排序到volatile寫以後。
  • 當第一個操做是volatile讀時,無論第二個操做是什麼,都不能重排序。這個規則確保volatile讀以後的操做不會被編譯器重排序到volatile讀以前。
  • 當第一個操做是volatile寫,第二個操做是volatile讀時,不能重排序。

爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。對於編譯器來講,發現一個最優佈置來最小化插入屏障的總數幾乎不可能,爲此,JMM採起保守策略。下面是基於保守策略的JMM內存屏障插入策略:

  • 在每一個volatile寫操做的前面插入一個StoreStore屏障。
  • 在每一個volatile寫操做的後面插入一個StoreLoad屏障。
  • 在每一個volatile讀操做的後面插入一個LoadLoad屏障。
  • 在每一個volatile讀操做的後面插入一個LoadStore屏障。

 

上述內存屏障插入策略很是保守,但它能夠保證在任意處理器平臺,任意的程序中都能獲得正確的volatile內存語義。

下面是保守策略下,volatile寫插入內存屏障後生成的指令序列示意圖:

上圖中的StoreStore屏障能夠保證在volatile寫以前,其前面的全部普通寫操做已經對任意處理器可見了。這是由於StoreStore屏障將保障上面全部的普通寫在volatile寫以前刷新到主內存。

這裏比較有意思的是volatile寫後面的StoreLoad屏障。這個屏障的做用是避免volatile寫與後面可能有的volatile讀/寫操做重排序。由於編譯器經常沒法準確判斷在一個volatile寫的後面,是否須要插入一個StoreLoad屏障(好比,一個volatile寫以後方法當即return)。爲了保證能正確實現volatile的內存語義,JMM在這裏採起了保守策略:在每一個volatile寫的後面或在每一個volatile讀的前面插入一個StoreLoad屏障。從總體執行效率的角度考慮,JMM選擇了在每一個volatile寫的後面插入一個StoreLoad屏障。由於volatile寫-讀內存語義的常見使用模式是:一個寫線程寫volatile變量,多個讀線程讀同一個volatile變量。當讀線程的數量大大超過寫線程時,選擇在volatile寫以後插入StoreLoad屏障將帶來可觀的執行效率的提高。從這裏咱們能夠看到JMM在實現上的一個特色:首先確保正確性,而後再去追求執行效率。

下面是在保守策略下,volatile讀插入內存屏障後生成的指令序列示意圖:

上圖中的LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序。LoadStore屏障用來禁止處理器把上面的volatile讀與下面的普通寫重排序。

上述volatile寫和volatile讀的內存屏障插入策略很是保守。在實際執行時,只要不改變volatile寫-讀的內存語義,編譯器能夠根據具體狀況省略沒必要要的屏障。下面咱們經過具體的示例代碼來講明:

class VolatileBarrierExample {
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite() {
        int i = v1;           //第一個volatile讀
        int j = v2;           // 第二個volatile讀
        a = i + j;            //普通寫
        v1 = i + 1;          // 第一個volatile寫
        v2 = j * 2;          //第二個 volatile寫
    }

    …                    //其餘方法
}

針對readAndWrite()方法,編譯器在生成字節碼時能夠作以下的優化:

 

注意,最後的StoreLoad屏障不能省略。由於第二個volatile寫以後,方法當即return。此時編譯器可能沒法準確判定後面是否會有volatile讀或寫,爲了安全起見,編譯器經常會在這裏插入一個StoreLoad屏障。

上面的優化是針對任意處理器平臺,因爲不一樣的處理器有不一樣「鬆緊度」的處理器內存模型,內存屏障的插入還能夠根據具體的處理器內存模型繼續優化。以x86處理器爲例,上圖中除最後的StoreLoad屏障外,其它的屏障都會被省略。

 

前面保守策略下的volatile讀和寫,在 x86處理器平臺能夠優化成:

前文提到過,x86處理器僅會對寫-讀操做作重排序。X86不會對讀-讀,讀-寫和寫-寫操做作重排序,所以在x86處理器中會省略掉這三種操做類型對應的內存屏障。在x86中,JMM僅需在volatile寫後面插入一個StoreLoad屏障便可正確實現volatile寫-讀的內存語義。這意味着在x86處理器中,volatile寫的開銷比volatile讀的開銷會大不少(由於執行StoreLoad屏障開銷會比較大)。

 

第五篇:鎖

http://www.infoq.com/cn/articles/java-memory-model-5

 

AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴於這些基礎類來實現的。從總體來看,concurrent包的實現示意圖以下:

 

 

也能夠結合這篇看:Java Synchronized和ReentrantLock的比較:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html

 

 

 

另有這個序列也講的不錯:

同步和Java內存模型

目錄

  1. 引言
  2. 原子性
  3. 可見性
  4. 有序性
  5. Volatile
相關文章
相關標籤/搜索