Java併發讀書筆記:JMM與重排序

Java內存模型(JMM)

Java內存模型(JMM)定義了程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。編程

在Java中,全部實例域、靜態域和數組元素都存在堆內存中,堆內存在線程之間共享,這些變量就是共享變量數組

局部變量(Local Variables),方法定義參數(Formal Method Parameters)和異常處理參數(Exception Handler Parameters)不會在線程之間共享,它們不存在內存可見性問題。緩存

JMM抽象結構

圖參考自《Java併發編程的藝術》3-1
安全

上圖是抽象結構,一個包含共享變量的主內存(Main Memory),出於提升效率,每一個線程的本地內存中都擁有共享變量的副本。Java內存模型(簡稱JMM)定義了線程和主內存之間的抽象關係,抽象意味着並不具體存在,還涵蓋了其餘具體的部分,如緩存、寫緩存區、寄存器等。多線程

此時線程A、B之間是如何進行通訊的呢?併發

  • A把本地內存中的更新的共享變量刷新到主內存中。
  • B再從主內存中讀取更新後的共享變量。

明確一點,JMM經過控制主內存與每一個線程的本地內存之間的交互,確保內存的可見性app

重排序

編譯器和處理器爲了優化程序性能會對指令序列進行從新排序,重排序可能會致使多線程出現內存可見性問題。性能

源碼->最終指令序列

下圖爲《Java併發編程的藝術》3-3
學習

編譯器重排序

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

JMM對於編譯器重排序規則會禁止特定類型的編譯器重排序。

處理器重排序

  • 指令級並行的重排序:現代處理器採用指令級並行技術(Instruction-Level-Parallelism,ILP)將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應及其指令的執行順序。
  • 內存系統的重排序:處理器使用緩存和讀/寫緩衝區,使得加載和存儲的操做看起來在亂序執行。

對於處理器重排序,JMM的處理器重排序會要求Java編譯器在生成指令序列時,插入特定類型的內存屏障指令,以禁止特定類型的處理器重排序。

數據依賴性

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

編譯器和處理器會遵照數據依賴性,不會改變存在數據依賴關係的兩個操做的執行順序。(針對單個處理器中執行的指令序列和單個線程中執行的操做)

考慮抽象內存模型,現代處理器處理線程之間數據的傳遞的過程:將數據寫入寫緩衝區,以批處理的方式刷新寫緩衝區,合併寫緩衝區對同一內存地址的屢次寫,減小內存總線的佔用。但每一個寫緩衝區只對它所在的處理器可見,處理器對內存的讀/寫操做可能就會改變。

as-if-serial

無論怎麼重排序,(單線程)程序的執行結果不能被改變,一樣,不會對具備數據依賴性的操做進行重排序,相應的,若是不存在數據依賴,就會重排序。

double pi = 3.14; // A 
double r = 1.0; // B 
double area = pi * r * r; // C
  • C與A訪問同一變量pi、C與B訪問同一變量r,且存在寫操做,具備依賴關係,它們之間不會進行重排序。
  • A與B之間不存在依賴關係,編譯器和處理器能夠重排序,能夠變成B->A->C。

很明顯,as-if-serial語義很好地保護了上述單線程,讓咱們覺得程序就是按照A->B->C的順序執行的。

happens-before

從JDK5開始,Java使用新的JSR-133內存模型,使用happens-before的概念闡述操做之間的內存可見性。

有個簡單的例子理解所謂的可見性和happens-before「先行發生」的規則。

i = 1;  //在線程A中執行
j = i;   //在線程B中執行

咱們對線程B中這個j的值進行分析:
假如A happens-before B,那麼A操做中i=1的結果對B可見,此時j=1,是確切的。但若是他們之間不存在happens-before的關係,那麼j的值是不必定爲1的。

在JMM中,若是一個操做執行的結果須要對另外一個操做可見,兩個操做能夠在不一樣的線程中執行,那麼這兩個操做之間必需要存在happens-before。

happens-before的規則

如下源自《深刻理解Java虛擬機》
意味着不遵循如下規則,編譯器和處理器將會隨意進行重排序。

  1. 程序次序規則(Program Order Rule):在一個線程內,按照程序代碼順序,書寫在前面的操做先行發生於書寫在後面的操做。
  2. 監視器鎖規則(Monitor Lock Rule):一個unLock操做在時間上先行發生於後面對同一個鎖的lock操做。
  3. volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操做在時間上先行發生於後面對這個量的讀操做
  4. 線程啓動規則(Thread Start Rule):Thread對象的start()先行發生於此線程的每個動做。
  5. 線程終止規則(Thread Termination Rule):線程中的全部操做都先行發生於對此線程的終止檢測。
  6. 線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。
  7. 對象終結規則(Finalizer Rule):一個對象的初始化完成先行發生於它的finalize()方法的開始。
  8. 傳遞性(Transitivity):A在B以前發生,B在C以前發生,那麼A在C以前發生。

happens-before關係的定義

  1. 若是A happens-before B,A的執行結果對B可見,且A的操做的執行順序排在B以前,即時間上先發生不表明是happens-before。
  2. A happens-before B,A不必定在時間上先發生。若是二者重排序以後,結果和happens-before的執行結果一致,就ok。

舉個例子:

private int value = 0;

public void setValue(int value){
    this.value = value;
}
public int getValue(){
    return value;
}

假設此時有兩個線程,A線程首先調用setValue(5),而後B線程調用了同一個對象的getValue,考慮B返回的value值:

根據happens-before的多條規則一一排查:

  • 存在於多個線程,不知足程序次序的規則。
  • 沒有方法使用鎖,不知足監視器鎖規則。
  • 變量沒有用volatile關鍵字修飾,不知足volatile規則。
  • 後面很明顯,都不知足。

綜上所述,最然在時間線上A操做在B操做以前發生,可是它們不知足happens-before規則,是沒法肯定線程B得到的結果是啥,所以,上面的操做不是線程安全的。

如何去修改呢?咱們要想辦法,讓兩個操做知足happens-before規則。好比:

  • 利用監視器鎖規則,用synchronized關鍵字給setValue()getValue()兩個方法上一把鎖。
  • 利用volatile變量規則,用volatile關鍵字給value修飾,這樣寫操做在讀以前,就不會修改value值了。

重排序對多線程的影響

考慮重排序對多線程的影響:
若是存在兩個線程,A先執行writer()方法,B再執行reader()方法。

class ReorderExample { 
    int a = 0; 
    boolean flag = false; 
    public void writer() { 
        a = 1;              // 1
        flag = true;        // 2 
    }
    Public void reader() { 
        if (flag) {         // 3 
            int i = a * a;  // 4
            …… 
        } 
    } 
}

在沒有學習重排序相關內容前,我會堅決果斷地以爲,運行到操做4的時候,已經讀取了修改以後的a=1,i也相應的爲1。可是,因爲重排序的存在,結果也許會出人意料。

操做1和2,操做3和4都不存在數據依賴,編譯器和處理器能夠對他們重排序,將會致使多線程的原先語義出現誤差。

順序一致性

數據競爭與順序的一致性

上面示例就存在典型的數據競爭

  • 在一個線程中寫一個變量。
  • 在另外一個線程中讀這個變量。
  • 寫和讀沒有進行同步。

咱們應該保證多線程程序的正確同步,保證程序沒有數據競爭。

順序一致性內存模型

  • 一個線程中的全部操做必須按照程序的順序來執行。
  • 全部線程都只能看到一個單一的操做執行順序。
  • 每一個操做都必須原子執行且馬上對全部線程可見

這些機制實際上能夠把全部線程的全部內存讀寫操做串行化

順序一致性內存模型和JMM對於正確同步的程序,結果是相同的。但對未同步程序,在程序順序執行順序上會有不一樣。

JMM處理同步程序

對於正確同步的程序(例如給方法加上synchronized關鍵字修飾),JMM在不改變程序執行結果的前提下,會在在臨界區以內對代碼進行重排序,未編譯器和處理器的優化提供便利。

JMM處理非同步程序

對於未同步或未正確同步的多線程程序,JMM提供最小安全性。

1、什麼是最小安全性?
JMM保證線程讀取到的值要麼是以前某個線程寫入的值,要麼是默認值(0,false,Null)。
2、如何實現最小安全性?
JMM在堆上分配對象時,首先會對內存空間進行清零,而後纔在上面分配對象。所以,在已清零的內存空間分配對象時,域的默認初始化已經完成(0,false,Null)
3、JMM處理非同步程序的特性?

  1. 不保證單線程內的操做會按程序的順序執行。
  2. 不保證全部線程看到一致的操做執行順序。
  3. 不保證64位的long型和double型的變量的寫操做具備原子性。(與處理器總線的工做機制密切相關)
  • 對於32位處理器,若是強行要求它對64位數據的寫操做具備原子性,會有很大的開銷。
  • 若是兩個寫操做被分配到不一樣的總線事務中,此時64位寫操做就不具備原子性。

總結

JMM遵循的基本原則:

對於單線程程序和正確同步的多線程程序,只要不改變程序的執行結果,編譯器和處理器不管怎麼優化都OK,優化提升效率,何樂而不爲。

as-if-serial與happens-before的異同

異:as-if-serial 保證單線程內程序的結果不被改變,happens-before 保證正確同步的多線程程序的執行結果不被改變。
同:二者都是爲了在不改變程序執行結果的前提下,儘量的提升程序執行的並行度


參考資料: 《Java併發編程的藝術》方騰飛 《深刻理解Java虛擬機》周志明

相關文章
相關標籤/搜索