【JVM】Java內存模型


Java內存模型,即JMM(Java Memery Model),它的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。Java虛擬機規范試圖通過定義這樣一種模型來屏蔽各種硬件和操作系統的內存訪問差異。

此處的變量是指包括實例字段、靜態字段和構成數組對象的元素,但不包括局部變量和方法參數,因為后者是線程私有的,不會被共享,不存在競爭問題。


主內存與工作內存

主內存和工作內存.png
  • 所有的變量都存儲在主內存,每條線程還有自己的工作內存。線程的工作內存保存了該線程使用到的變量的主內存的副本。(這里的副本拷貝并不是真正的內存復制,虛擬機會通過不同的實現方式達到這種效果)。
  • 線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。
  • 不同線程之間不能直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成。
  • 從更低層次來將,主內存直接對應于物理硬件的內存,而為了獲取更好的運行速度,虛擬機可能會讓工作內存優先存儲于寄存器和高速緩存中,因為程序運行時主要訪問讀寫的是工作內存。

內存間交互操作

Java內存模型中定義了8種操作來完成主內存和工作內存之間的交互:如何將變量從主內存拷貝到工作內存、如何將變量從工作內存同步回主內存。

這8中操作每一個都是原子性的、不可再分的。

操作 變量域 作用
lock 主內存 將變量標識為一條線程獨占的狀態
unlock 主內存 將處于鎖定狀態的變量釋放,之后才可以再被其他線程鎖定
read 主內存 將變量值從主內存傳輸到工作內存
load 工作內存 將read操作從主內存得到的變量放入工作內存的變量副本中
use 工作內存 將工作內存中的變量的值傳遞給執行引擎使用
assign 工作內存 把從執行引擎中接受到的值賦給工作內存
store 工作內存 將工作內存中變量的值傳輸到主內存中
write 主內存 將store操作從工作內存中得到的變量的值放入主內存中的變量中

假如主內存中存在變量var,工作內存中存在變量var的副本var_copy:先通過對var執行read操作,將var的值傳輸到工作內存中;再對var_copy執行load操作將傳來的值賦給var_copy;當執行引擎執行使用到該變量的字節碼指令時,對var_copy執行use操作將var_copy的值傳輸給執行引擎;執行引擎執行結束后,對var_copy執行assign操作,將執行引擎傳輸來的值賦給var_copy;對var_copy執行store操作,將var_copy的值傳輸到主內存,對var執行write操作,將傳輸來的值賦給var

執行上述8種操作時必須滿足如下8種規則:

  • 執行順序相關的規則
  • read load必須同時出現,store write必須同時出現
  • 執行assign后就必須執行store write:變量在工作內存改變后必須同步回主內存
  • 不執行assign就不能必須store write:不能無原因地將數據從工作內存同步到主內存
  • use之前必須先read load:不能直接使用主內存中的變量
  • lock和unlock相關的規則
  • 一個變量在同一時刻只允許一個線程對其進行lock操作,但可被同一線程多次lock,多次lock后必須執行相同次數的unlock,變量才能被解鎖。
  • 對一個變量執行lock,就會清空工作內存中該變量的值,在執行引擎使用這個變量的時候,必須重新loadassign來初始化變量的值。
  • 不能去unlock一個沒有lock的變量,不能unlock被其他線程lock的變量
  • 對變量unlock之前,必須先store write:解鎖前必須將變量同步到主內存中。

原子性、可見性、有序性

Java內存模型是圍繞著在并發過程中如何處理原子性、可見性、有序性這三個特征來建立的。

  • 原子性
    這里所說的原子性是指某操作具有原子性,即原子操作。
    原子操作是指不會被線程調度機制打斷的操作,這種操作一旦開始,就一直運行到結束,中間不會切換到另一個線程。
    ①Java內存模型中的原子操作包括read、load、use、assign、store、write。
    ②Java語法層面,基本數據類型的訪問讀寫都是原子操作。
比如a=5這個給a賦值的操作時原子性的,但a = a + 1就不是原子性的;

但是,在實際應用中,通常需要更大范圍的原子性保證,比如上面的a = a + 1操作。Java內存模型提供了lock和unlock來滿足這種需求,雖然這兩個操作并未開放給用戶使用,但卻提供了更高層次的字節碼指令monitorenter和monitorexit來隱士地使用這兩個操作。這兩個字節碼指令反映到Java代碼中就是同步塊——synchronized關鍵字。因此在synchronized塊之間的操作也具備原子性。

  • 可見性
    是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。
    Java內存模型是通過將新值同步回主內存,在變量讀取前從主內存刷新變量這種依賴主內存作為傳遞媒介的方式來實現可見性的。
    ①無論是普通變量還是volatile變量都是如此,只不過volatile關鍵字保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。
    除了volatile關鍵字,synchronizedfinal關鍵字也能實現可見性。同步塊的可見性是由“對變量unlock之前,必須先store write將變量從工作內存同步到主內存”這條規則獲得的。而final的可見性是指:被final修飾的字段在構造器中一旦初始化完成,并且構造器沒有把this的引用傳遞出去,那在其他線程就能看見final字段的值。

  • 有序性

  • Java程序中天然的有序性
    ①如果在本線程內觀察,所有的操作都是有序的(線程內表現為串行的語義)
    ②如果在一個線程觀察另外一個線程,所有的操作都是無序的(指令重排序現象和工作內存與主內存同步延遲現象)

  • volatilesynchronized保證線程之間的有序性
    volatile本身就包含了禁止指令重排序的語義
    synchronized則是由“一個變量一個時刻只允許一條線程對其進行lock”這條規則獲得的,這條規則決定了持有同一個鎖的同步塊只能串行地進入。


volatile型變量的特殊規則

Java內存模型對volatile專門定義了一些特殊的訪問規則。

  • read load use必須連續出現,中間不能出現別的操作(每次use之前必須是load):保證看見其他線程的對變量的修改。
  • assign store write必須連續出現,中間不能出現別的操作(每次assign之后必須是store):保證每次在工作內存中修改變量值后都立即同步到主內存。
  • 兩個volatile型變量A和B,如果線程T對A的use(或assign)先于線程T對B的use(或assign),那么線程T對A的read(或write)先于線程T對B的read(或write):保證volatile修飾的變量不會被重排序優化,保證代碼的執行順序與程序的順序相同。

通過上述的訪問規則可以看出,volatile的實現(或者說volatile的同步機制)是沒有用到lock操作的(無鎖)。

正是由于上述特殊的訪問規則,才使得volatile關鍵字修飾的變量具有如下兩個特征:

  • 可見性
    當一個變量被定義為volatile之后,當一個線程修改了這個變量的值,其他線程可以立即得知。假如有變量var,線程1和線程2,如果var是普通變量,線程1修改var的值之后,然后向主內存進行回寫。線程2假如在線程1修改var值之后、回寫之前對變量var進行了讀操作,此時仍然是舊值(不安全)。如果var是volatile變量,那么無論線程2何時讀var的值,var的值都是最新的:對volatile變量的所有的寫操作都能立即反應到其他線程中,volatile變量對所有線程是立即可見的。但由于Java中的運算并不是原子的,如果對volatile變量進行并發運算,一樣是不完全的。
    滿足以下兩條規則的場景可以保證volatile的并發安全:
    ①運算結果不依賴volatile變量的當前值,或者能夠保證只有單一的線程修改volatile變量的值;
    ②volatile變量不需要與其他的狀態變量共同參與不變約束。
    ③下面這種場景很適合volitile來控制并發,當調用stop()方法后,能保證所有獻線程中執行的doWork()方法都立即停下來。
volatile boolean shouldStop = false;
public void stop(){
      shouldStop = true;
}    
public void doWork(){
      while (!shouldStop){
            //do something
      }
}
  • 禁止指令重排序優化
    普通變量僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,但不會保證變量賦值操作的順序與程序代碼中的執行順序一致。(線程內表現為串行的語義)。但被volatile修飾的變量,可以保證該變量賦值的順序與程序代碼中的執行順序一致。
//
volatile boolean isInitialized = false;
//
//假設以下代碼在線程A中執行
void initial(){
    //此處是加載配置文件的代碼
    isInitialized = true;
}
//
//假設以下代碼在線程B中執行,此方法依賴線程A中加載完配置文件
void doWork(){
    while (isInitialized){
        //do something 
    }
}

如果變量isInitialized沒有volatile修飾,有可能線程A中的initial()方法會先執行isInitialized = true;,再執行加載配置文件的代碼,就會導致線程B在執行doWork()方法的時候報錯。


先行發生原則

  • 概念
    先行發生(happens-before)是Java內存模型中定義的兩項操作之間的偏序關系,如果說操作A先行發生于操作B(發生在操作B之前),那么操作A產生的影響能被B觀察到。這里的影響包括:修改了內存中的共享變量的值、發送了消息、調用了方法等。

  • 天然的先行發生關系

  • 程序次序規則:在同一個線程內,按照程序代碼順序,書寫在前面的操作先行發生于書寫在后面的操作。

  • 管程鎖定規則:一個unlock操作先行發生于后面對于同一個鎖的lock操作。這里的后面,是指時間上的先后順序。

  • volatile變量規則:對一個volatile變量的寫操作先行發生于后面對這個變量的讀操作。這里的后面,是指時間上的先后順序。

  • 線程啟動規則:Thread對象的start()方法先行發生于此線程的每一個動作。

  • 線程終止規則:線程中的所有操作都先行發生于對此線程的終止檢測,比如可以通過Thread.join()方法、Thread.isAlive()方法的返回值等手段檢測到線程已經終止。

  • 線程中斷規則:對線程interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生??梢酝ㄟ^Thread.interrupted()方法檢測到是否有中斷發生。

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

  • 傳遞性:如果A先行發生于B,B先行發生于C,那么A先行發生于C。

注意:一個操作時間上的先發生不代表這個操作會先行發生,同樣,一個先行發生的操作不代表時間上的先發生(指令重排序等)。

public class HappensBefore {

    private int value = 0;

    public int getValue() {
        return value;
    }

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

上述代碼中,如果線程A先(時間上的先)調用了setValue(1)方法,線程B后(時間上的后)調用了getValue(),那么B有可能獲取的value值是0,而不一定是1。


內容摘抄自《深入理解Java虛擬機》

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容