目錄java
Java內存模型(JMM)定義了程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。編程
在Java中,全部實例域、靜態域和數組元素都存在堆內存中,堆內存在線程之間共享,這些變量就是共享變量。數組
局部變量(Local Variables),方法定義參數(Formal Method Parameters)和異常處理參數(Exception Handler Parameters)不會在線程之間共享,它們不存在內存可見性問題。緩存
圖參考自《Java併發編程的藝術》3-1
安全
上圖是抽象結構,一個包含共享變量的主內存(Main Memory),出於提升效率,每一個線程的本地內存中都擁有共享變量的副本。Java內存模型(簡稱JMM)定義了線程和主內存之間的抽象關係,抽象意味着並不具體存在,還涵蓋了其餘具體的部分,如緩存、寫緩存區、寄存器等。多線程
此時線程A、B之間是如何進行通訊的呢?併發
明確一點,JMM經過控制主內存與每一個線程的本地內存之間的交互,確保內存的可見性。app
編譯器和處理器爲了優化程序性能會對指令序列進行從新排序,重排序可能會致使多線程出現內存可見性問題。性能
下圖爲《Java併發編程的藝術》3-3
學習
JMM對於編譯器重排序規則會禁止特定類型的編譯器重排序。
對於處理器重排序,JMM的處理器重排序會要求Java編譯器在生成指令序列時,插入特定類型的內存屏障指令,以禁止特定類型的處理器重排序。
若是兩個操做訪問同一變量,且這兩個操做中有一個爲寫操做,此時這兩個操做之間就存在數據依賴性。
編譯器和處理器會遵照數據依賴性,不會改變存在數據依賴關係的兩個操做的執行順序。(針對單個處理器中執行的指令序列和單個線程中執行的操做)
考慮抽象內存模型,現代處理器處理線程之間數據的傳遞的過程:將數據寫入寫緩衝區,以批處理的方式刷新寫緩衝區,合併寫緩衝區對同一內存地址的屢次寫,減小內存總線的佔用。但每一個寫緩衝區只對它所在的處理器可見,處理器對內存的讀/寫操做可能就會改變。
無論怎麼重排序,(單線程)程序的執行結果不能被改變,一樣,不會對具備數據依賴性的操做進行重排序,相應的,若是不存在數據依賴,就會重排序。
double pi = 3.14; // A double r = 1.0; // B double area = pi * r * r; // C
很明顯,as-if-serial
語義很好地保護了上述單線程,讓咱們覺得程序就是按照A->B->C的順序執行的。
從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。
如下源自《深刻理解Java虛擬機》
意味着不遵循如下規則,編譯器和處理器將會隨意進行重排序。
start()
先行發生於此線程的每個動做。interrupt()
方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。finalize()
方法的開始。舉個例子:
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
的多條規則一一排查:
綜上所述,最然在時間線上A操做在B操做以前發生,可是它們不知足happens-before
規則,是沒法肯定線程B得到的結果是啥,所以,上面的操做不是線程安全的。
如何去修改呢?咱們要想辦法,讓兩個操做知足happens-before
規則。好比:
synchronized
關鍵字給setValue()
和getValue()
兩個方法上一把鎖。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對於正確同步的程序,結果是相同的。但對未同步程序,在程序順序執行順序上會有不一樣。
對於正確同步的程序(例如給方法加上synchronized關鍵字修飾),JMM在不改變程序執行結果的前提下,會在在臨界區以內對代碼進行重排序,未編譯器和處理器的優化提供便利。
對於未同步或未正確同步的多線程程序,JMM提供最小安全性。
1、什麼是最小安全性?
JMM保證線程讀取到的值要麼是以前某個線程寫入的值,要麼是默認值(0,false,Null)。
2、如何實現最小安全性?
JMM在堆上分配對象時,首先會對內存空間進行清零,而後纔在上面分配對象。所以,在已清零的內存空間分配對象時,域的默認初始化已經完成(0,false,Null)
3、JMM處理非同步程序的特性?
對於單線程程序和正確同步的多線程程序,只要不改變程序的執行結果,編譯器和處理器不管怎麼優化都OK,優化提升效率,何樂而不爲。
異:as-if-serial 保證單線程內程序的結果不被改變,happens-before 保證正確同步的多線程程序的執行結果不被改變。
同:二者都是爲了在不改變程序執行結果的前提下,儘量的提升程序執行的並行度。
參考資料: 《Java併發編程的藝術》方騰飛 《深刻理解Java虛擬機》周志明