Java內存模型

Java內存模型指定了JVM如何與計算機內存協同工做。JVM是整個計算機的模型所以這個模型包含了內存模型,也就是Java內存模型。java

若是你像要設計正確行爲的併發程序,那麼瞭解Java內存模型是很是重要的。Java內存模型指定了如何以及什麼時候不一樣的線程可以看到其餘線程寫入共享變量的值,以及如何在須要的時候如何同步訪問共享變量。緩存

最初的Java內存模型是不足的,所以Java內存模型在Java1.5作了改進,這個版本的Java內存模型在Java8中仍然被使用。多線程

內部的Java內存模型

Java內存模型在JVM內部使用,將內存分爲了線程棧和堆。下面的圖從邏輯角度給出了Java內存模型:架構

clipboard.png

每一個運行在JVM內部的線程都有本身的線程棧。線程棧包含關於線程調用的哪一個方法到達了當前執行點的信息。我對此引用爲「調用棧」。隨着線程執行代碼,調用棧會發生變化。併發

調用棧還包含每一個被執行的方法的全部本地變量(全部調用棧上的方法)。一個線程只可以訪問它本身的線程棧。由一個線程建立的本地變量對其餘線程不可見。即便兩個線程執行同一段代碼,這兩個線程也會在他們各自的線程棧中建立這段代碼涉及的本地變量。所以,每一個線程都有本身版本的本地變量。spa

全部內建類型的本地變量(boolean,byte,short,char,int,long,float,double)被存儲在線程棧而且對其餘線程不可見。一個線程可能會傳遞一個內建類型變量的副本給其餘線程,可是它不會貢獻它本身的內建本地變量。線程

堆包含了你的Java程序中建立的全部對象,不論是哪一個線程建立的。這包含了對象版本的內建類型(如Byte,Integer,Long等等)。若是一個對象唄建立並被複制給一個本地變量,或者被建立爲一個成員變量都是不要緊的,對象仍然存儲在堆上。設計

下圖給出了調用棧和存儲在線程棧中的本地變量,以及存儲在堆上的對象:code

clipboard.png

一個本地變量多是一個內建類型,這種狀況它徹底存儲在線程棧。對象

一個本地變量多是一個對象的引用。這種狀況這個引用(本地變量)存儲在線程棧中,可是對象自己存儲在堆上。

一個對象可能包含方法,而且這些方法可能包含本地變量。這些本地變量存儲在線程棧,即便方法所屬對象存儲在堆上。

一個對象的成員變量和對象一塊兒存儲在堆上。對於成員變量是內建類型,或者它是對象的引用都是如此。

靜態類變量和類定義一塊兒存儲在堆上。

堆上的對象可以被全部擁有這個對象引用的線程訪問。當一個線程訪問一個對象,它也能夠訪問這個對象的成員變量。若是兩個線程在同一個對象上同時調用它的同一個方法,這兩個線程會同時又權限訪問這個對象的成員變量,可是每一個線程會有它本身的本地變量副本。

下面的圖給出了上面所說的:

clipboard.png

兩個線程有同一組本地變量。一個本地變量(Local Variable 2)指向了堆上的一個共享對象(Object3)。每一個線程都有對同一個對象的不一樣引用。它們的引用是本地變量而且存儲在各自的線程棧上,儘管這兩個不一樣的引用指向堆上的同一個對象。

注意共享對象(Object 3)有一個對Object2和Object4的引用做爲它的成員變量,經過Object3中的這些成員變量引用,這兩個線程能夠訪問Object2和Object4。

圖中還給出了一個本地變量指向堆上的兩個不一樣的對象。這個例子中引用指向了兩個不一樣對象(Object1和Object5),而不是同一個對象。理論上全部線程若是有指向全部有對象的引用,那麼這些線程能夠訪問到Object1和Object5。可是在圖中每一個線程只有一個引用指向這兩個對象之一。

那麼,什麼樣的Java代碼可以知足上面的內存圖示?請看下面的簡單代碼:

public class MyRunnable implements Runnable {
  
  public void run() {
    methodOne();
  }

  public void methodOne() {
    int localVariable1 = 45;
    
    MyShareObject localVariable2 = MyShareObject.shareInstance;
    
    // ... do more with local variables.
    
    methodTwo();
  }

  public void methodTwo() {
    Integer localVariable1 = new Integer(99);
    
    // ... do more with local variable.
  }
}
public class MyShareObject {
  
  // static variable pointing to instance of MyShareObject

  public static final MySharedObject sharedInstance = new MySharedObject();

  // member variable pointing to two objects on the heap

  public Integer object2 = new Integer(22);
  public Integer object4 = new Integer(44);

  public long member1 = 12345;
  public long member2 = 67890;
}

若是兩個線程執行run()方法,則圖中所示就是結果。run()方法調用methodOne()而後methodOne()調用methodTwo()。

methodOne()聲明瞭一個內建類型的本地變量(int類型的localVariable1),另外一個本地變量是一個對象的引用(localVariable2)。

每一個執行methodOne()的線程會在各自的線程棧上建立它本身的localVariable1和localVariable2的副本。兩個localVariable1變量徹底和對方沒有關係,只是活在各自的線程棧上。一個線程不能看到另外一個線程它本身的localVariable1副本變化。

每一個執行methodOne()的線程也會在各自的線程棧上建立它們本身的localVariable2副本。然而這兩個不一樣的localVariable2副本是指向堆上的同一個對象。代碼設置localVariable2指向被一個靜態變量引用的對象。這裏只有一個靜態變量的副本而且這個副本存儲在堆上。所以全部localVariable2的這兩個副本都指向同一個被靜態變量指向的MySharedObject實例。MySharedObject實例存儲在堆上,它對應圖上的Object3。

注意MySharedObject類還包含了兩個成員變量。成員變量和這個對象一塊兒存儲在堆上。這兩個成員變量指向了兩個Integer對象。這些Integer對象對應圖上Object2和Object4。

注意methodTwo()建立了一個名爲localVariable1的本地變量,這個本地變量是一個Integer對象的引用。這個方法設置localVariable1引用指向了一個新的Integer實例。localVariable1引用會存儲在執行methodTwo()方法的每一個線程的副本中。兩個被實例化的Integer對象會存儲在堆中,可是因爲每次方法執行時都建立了一個新的Integer對象,兩個線程會執行並建立兩個不一樣的Integer實例。methodTwo()中建立的Integer對象對應圖中的Object1和Object5。

注意MySharedObject中的兩個long型的成員變量是內建類型。因爲這些變量的成員變量,所以它們仍然和對象一塊兒存儲在堆上。只有本地變量會存儲在線程棧上。

硬件內存架構

現代硬件內存架構和內部Java內存模型有些區別。對於瞭解Java內存模型如何工做,瞭解硬件內存架構也很重要。這部分描述通用硬件內存架構,下一個部分會描述Java內存模型是如何工做在硬件內存之上。

這裏有一個簡單的計算機硬件架構模型:

clipboard.png

現代計算機一般有2個或更多的CPU。有些CPU還有多個核。重點是,在一個有2個或更多CPU的計算機上,有多個線程同時運行是可能的。每一個CPU可以在任什麼時候候運行一個線程。這意味着若是你的Java程序是多線程的,每一個CPU一個線程同時併發運行在你的Java程序中。

每一個CPU包含一組寄存器,本質行是CPU內的存儲。CPU在這些寄存器中執行操做會比在主存中快的多。這是由於CPU可以更快的訪問這些寄存器。

每一個CPU可能還有一個CPU緩存層。實際上,大部分現代CPU都有一個特定大小的緩存層。CPU能比訪問主存更快的訪問緩存,可是通常不會比訪問它的內部寄存器更快。所以,CPU緩存是一個介於內部寄存器和主存之間的地方。有些CPU可能有多級緩存(Level1和Level2),可是這對理解Java內存模型如何與內存交互來講並非很須要知道。

一個計算機也包含一個主存區域(RAM)。全部CPU都能訪問主存。主存區域比CPU緩存大的多。

通常來講,當一個CPU須要訪問主存,它會將主存的一本讀取到它的CPU緩存。甚至它可能會讀取部分緩存到它的內部寄存器並在其上操做。當CPU須要將結果寫回到主存它會將值從內部寄存器刷到緩存,在摸個時間點將緩存中的值刷回到主存。

當CPU須要在緩存中存儲一些其餘東西時,緩存中存儲的值會被刷回到主存。每次緩存更新時,CPU沒必要讀寫整塊緩存。對於緩存在較小內存塊上的更新的標準說法是「cache lines」。一個或多個cache lines會被讀到緩存,一個或多個cache lines會被刷回主存。

鏈接Java內存模型和硬件內存架構

上面說道,Java內存模型和硬件內存架構不一樣。硬件內存架構不會分辨線程棧和堆。在硬件上,線程棧和堆都定位到主存。部分線程棧和堆可能在某些時候會佔用CPU緩存和內部CPU寄存器。以下圖所示:

clipboard.png

當對象和變量能被存儲在計算機的不一樣內存區域時,特定的問題就會發生。兩個主要問題是:

  • 線程更新(寫)到共享變量的可見性
  • 讀寫檢查共享變量時發生的競態條件

這些問題會在下面的部分解釋。

共享變量的可見性

若是兩個或多個線程共享一個對象,若是沒有恰當使用volatile聲明或者同步,一個線程對共享變量的更新對其餘線程可能會不可見。

想象一個共享對象初始存儲在主存。一個運行在CPU1上的線程將這個共享變量讀取到它的CPU緩存,而後對這個共享變量作一些改變,只要CPU緩存沒有被刷回主存,這個共享變量的變動版本對運行在其餘CPU上的線程就是不可見的。這種方式每一個線程會有這個共享變量的本地副本,每一個副本位於不一樣的CPU緩存中。

下圖展現了這種狀況。運行在左邊CPU的線程將共享變量拷貝到它的CPU緩存,並將這個對象的count變量變爲2.這個變化對運行在右邊CPU上的線程不可見,由於對count的更新尚未刷回主存。

clipboard.png

爲了解決這個問題,你可使用Kava的volatile關鍵字。volatile關鍵字可以保證一個給定的變量從主存中讀取,而且當變量更新時會寫回主存。

競態條件

若是兩個或多個線程共享一個對象,多餘一個線程更新這個共享對象的變量,靜態條件就可能發生。

想象若是線程A讀取了一個共享對象的count變量到它的CPU緩存,線程B作一樣的事情,可是是在一個不一樣的CPU緩存。如今線程A對count加1,線程B也對count加1.如今count被加了兩次,每次都是在不一樣的CPU緩存。

若是這些增長的操做被順序執行,那麼變量count會增長兩次並有初始值+2的值被寫回主存。

可是這兩次增長是在沒有同步的狀況下併發操做的。無論線程A仍是線程B將它們對count的更新版本寫回主存,count只會獲得初始值+1,儘管有兩次更新。

下面的圖描述了靜態條件:

clipboard.png

爲了解決這個問題你能夠用一個synchronized塊。一個synchronized塊保證了同時只有一個線程能進入一個給定的關鍵代碼區域。synchronized塊也保證了全部在synchronized塊中訪問的變量會從主存中讀取,當一個線程退出synchronized塊,全部對變量的更新會再次刷回主存,無論這個變量是否被聲明爲volatile。

相關文章
相關標籤/搜索