Java 內存模型- Java Memory Model


  多線程越來越多的使用,使得我們需要對它的深入理解。那么就涉及到了Java內存模型JMM。JMM是JVM的一部分,JMM定義了一個線程修改了一個共享變量,其他線程什么時候或者如何看到這個變量,如何去訪問共享變量。

  咱們來看一張圖(圖片手繪的,字寫的不好,見諒),JVM里邊分為堆和棧,每一個線程都有一個線程棧,用於區分其他線程。

  每個線程的入口是一個run方法,然后run方法開始調用其他方法。在方法中有兩種數據類型,一種是原始類型,一種是引用類型。原始類型如( boolean, byte, short, char, int, long, float, double),這種其他線程根本看不到,只在線程中使用(存放在線程調用棧中)。另外一種是引用類型,引用類型比如原始類型的對象類型,比如(Boolean,Byte等),線程使用的時候會在堆中生成一個對象,並且將變量對其進行指向。每個線程使用此種變量的時候會自己創建一個變量(副本)並且指向,只有當前線程知道,其他線程看不到。那么引用對象的成員變量又分為基本類型和原始類型,並且一次進行創建副本並且指向。

   在方法中用到static對象的實例的需要進行區別對待。因為在堆里邊只有一個對象,所有線程對其進行引用。此時不是副本,需要注意。那么如果多線程對其驚醒操作的時候會出現值寫的不對的,比如兩個線程同時對對象里邊的成員變量原始int類型count進行加1,如果count初始化的0的話,可能會出現結果為1的情況。

  那么如果傳過來的參數分為基本類型和引用類型呢?如果為基本類型,那么就是副本,如果是引用類型的話,就是原始對象的副本和副本指向,在這時需要注意,如果改了內存中對象的屬相,那么隨之這個對象會發生改變,但是對象的指向不會改變。

  下面咱們通過代碼才詳細看一下,看之前首先看看圖片。

  

   咱們先進行簡單的解釋,一個對象,里邊有兩個方法,第一個方法只有一個原始類型變量,第二個方法有兩個變量,一個是原始對象引用類型,另一個是靜態對象實例的引用類型。這個圖就是他們的內存結構圖。下面咱們來看看代碼:

package com.hqs.jmm;

/**
 * 
 * JMM對象
 * @author qs.huang
 *
 */
public class MyJMMObject implements Runnable{

    @Override
    public void run() {
        methodOne();
    }
    
    public void methodOne() {
        int var1 = 0; //方法內部變量,原始類型
        methodTwo();
    }
    
    public void methodTwo(){
        Integer var1 = new Integer(0); //方法內部變量,引用變量
        MyReferenceObj var2 = MyReferenceObj.instance; //靜態引用變量
    }
    
    
    
}
package com.hqs.jmm;


/**
 * 
 * 引用對象
 * @author qs.huang
 *
 */

public class MyReferenceObj {
    public static MyReferenceObj instance = new MyReferenceObj();
    public Integer intObj = new Integer(1);
    public int intPrimary = 0;
    //隱藏構造
    private MyReferenceObj(){}
}

  因為中的var1是方法1的局部變量,也是原始類型,每個用到它的地方,把它放到方法棧中。每一個線程都有自己的棧,所以每個一份。

  方法2中的var1是一個對象類型的也是局部的,每個線程需要在堆里邊創建一個對象,同時對它進行指向。

  方法2中var2是一個靜態對象實例的引用,所以再堆中只有一份,並且在加載的時候進行的實例化,因為對象中還有引用類型,所以產生了一個對intObj的引用,同時還有一個int原始類型存在堆里,跟隨實例對象。

  那么方法中如果有基本類型的數組呢?那數組會在堆中生成,然后對象只想堆中的數組對象,多線程的話,每個線程會生成自己所需要的副本,當方法調用完成后,該數組對象就會被收回。

  下面咱們來看看CPU的硬件結構以便大家更理解JMM。看圖:

    目前的電腦一般都是2個或2個以上的CPU,每個CPU可能是多核的。那么每個CPU在同一時間就可以處理一個線程,多個CPU就可以同一時間執行多個線程。

  每個CPU都有一個寄存器,CPU通過寄存器進行運算,那么在寄存器運算速度要高於在主內存進行運算。

  每個CPU都附帶一個緩存,用於將數據從主內存中讀取到緩存數據中,然后再運算的時候放到寄存器里。CPU訪問寄存器的速度是最快的,訪問緩存的速度其次,最后是訪問主內存的速度,當然緩存分為L1,L2 兩個緩存,當然我畫的沒有那么好,不過不影響理解。CPU不會讀取緩存中的所有數據,而是按照緩存line去進行有選擇的讀取。

  當CPU執行完相關的運算並在適當的時候將結果刷到主內存RAM中,用於保存結果或讓其他程序讀取。咱們看一下JVM和CPU之間的關系。

  因為CPU沒有堆和棧,JVM的堆和棧會在CPU的主內存中,但是程序執行的時候,會將棧或堆中的線程讀取到緩存和寄存器中進行運算,並且將計算的結果重新刷新到主內存RAM中。在這個時候因為有多CPU的原因,那么假如說一個CPU一個變量,那么兩個並行的線程在執行的時候會有什么樣的問題呢?

  比如一個類中只有一個String state的成員變量,一個線程對其進行讀取到CPU緩存中,然后將其設置為了'YES',並放回到緩存中;另一個線程沒有看到這個值的更改,因為沒有看到起更改。然后將其讀取到CPU緩存中,然后設置為'YES'或'NO'。這個就是可見性問題,那么如何實現其他線程可見呢?Java有個關鍵字volatile,這個關鍵字可以使得操作不寫入CPU緩存,直接從主內存讀取,更改后直接重寫到主緩存中。

  比如這個類有個int count的成員變量,並且初始化值為0,向前面提到的,一個線程讀取到這個count到CPU緩存中,另一個線程也把這個count讀取到另一個CPU的線程中,那么兩個線程放到寄存器計算,分別對其進行加1操作,這個時候都把結果刷新到緩存並且到主內存中。count的結果變為了1,這個不是大家想要的。因為每個線程對這個變量讀取不可見,每個都用其副本進行操作。這個就是線程的競態條件。那么怎么才能都保證這個變量的正確呢,就是使用同步,也就是使用synchronize關鍵字或者是鎖來進行處理。也就是在同一時間只能有一個線程去處理這個字段或者方法,同時程序也是從主內存讀取數據,然后計算完成后將程序寫入到主內存中保證保證計算的有序處理。

  這下同學們是否有了新的認識了呢?

  如果有寫的不對的地方希望告知~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM