Java內存模型規範了Java虛擬機與計算機內存是如何協同工做的。Java虛擬機是一個完整的計算機的一個模型,所以這個模型天然也包含一個內存模型——又稱爲Java內存模型。html
若是你想設計表現良好的併發程序,理解Java內存模型是很是重要的。Java內存模型規定了如何和什麼時候能夠看到由其餘線程修改事後的共享變量的值,以及在必須時如何同步的訪問共享變量。java
原始的Java內存模型存在一些不足,所以Java內存模型在Java1.5時被從新修訂。這個版本的Java內存模型在Java8中人在使用。緩存
Java內存模型把Java虛擬機內部劃分爲線程棧和堆。這張圖演示了Java內存模型的邏輯視圖。多線程
每個運行在Java虛擬機裏的線程都擁有本身的線程棧。這個線程棧包含了這個線程調用的方法當前執行點相關的信息。一個線程僅能訪問本身的線程棧。一個線程建立的本地變量對其它線程不可見,僅本身可見。即便兩個線程執行一樣的代碼,這兩個線程任然在在本身的線程棧中的代碼來建立本地變量。所以,每一個線程擁有每一個本地變量的獨有版本。架構
全部原始類型的本地變量都存放在線程棧上,所以對其它線程不可見。一個線程可能向另外一個線程傳遞一個原始類型變量的拷貝,可是它不能共享這個原始類型變量自身。併發
堆上包含在Java程序中建立的全部對象,不管是哪個對象建立的。這包括原始類型的對象版本。若是一個對象被建立而後賦值給一個局部變量,或者用來做爲另外一個對象的成員變量,這個對象任然是存放在堆上。spa
下面這張圖演示了調用棧和本地變量存放在線程棧上,對象存放在堆上。線程
一個本地變量多是原始類型,在這種狀況下,它老是「呆在」線程棧上。設計
一個本地變量也多是指向一個對象的一個引用。在這種狀況下,引用(這個本地變量)存放在線程棧上,可是對象自己存放在堆上。code
一個對象可能包含方法,這些方法可能包含本地變量。這些本地變量任然存放在線程棧上,即便這些方法所屬的對象存放在堆上。
一個對象的成員變量可能隨着這個對象自身存放在堆上。無論這個成員變量是原始類型仍是引用類型。
靜態成員變量跟隨着類定義一塊兒也存放在堆上。
存放在堆上的對象能夠被全部持有對這個對象引用的線程訪問。當一個線程能夠訪問一個對象時,它也能夠訪問這個對象的成員變量。若是兩個線程同時調用同一個對象上的同一個方法,它們將會都訪問這個對象的成員變量,可是每個線程都擁有這個本地變量的私有拷貝。
下圖演示了上面提到的點:
兩個線程擁有一些列的本地變量。其中一個本地變量(Local Variable 2)執行堆上的一個共享對象(Object 3)。這兩個線程分別擁有同一個對象的不一樣引用。這些引用都是本地變量,所以存放在各自線程的線程棧上。這兩個不一樣的引用指向堆上同一個對象。
注意,這個共享對象(Object 3)持有Object2和Object4一個引用做爲其成員變量(如圖中Object3指向Object2和Object4的箭頭)。經過在Object3中這些成員變量引用,這兩個線程就能夠訪問Object2和Object4。
這張圖也展現了指向堆上兩個不一樣對象的一個本地變量。在這種狀況下,指向兩個不一樣對象的引用不是同一個對象。理論上,兩個線程均可以訪問Object1和Object5,若是兩個線程都擁有兩個對象的引用。可是在上圖中,每個線程僅有一個引用指向兩個對象其中之一。
所以,什麼類型的Java代碼會致使上面的內存圖呢?以下所示:
public class MyRunnable implements Runnable() { public void run() { methodOne(); } public void methodOne() { int localVariable1 = 45; MySharedObject localVariable2 = MySharedObject.sharedInstance; //... do more with local variables. methodTwo(); } public void methodTwo() { Integer localVariable1 = new Integer(99); //... do more with local variable. } } public class MySharedObject { //static variable pointing to instance of MySharedObject public static final MySharedObject sharedInstance = new MySharedObject(); //member variables 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 member1 = 67890; }
若是兩個線程同時執行
聲明瞭一個原始類型的本地變量和一個引用類型的本地變量。run()
方法,就會出現上圖所示的情景。run()
方法調用methodOne()
方法,methodOne()
調用methodTwo()
方法。methodOne()
每一個線程執行methodOne()
都會在它們對應的線程棧上建立localVariable1
和localVariable2
的私有拷貝。localVariable1
變量彼此徹底獨立,僅「生活」在每一個線程的線程棧上。一個線程看不到另外一個線程對它的localVariable1
私有拷貝作出的修改。
每一個線程執行methodOne()
時也將會建立它們各自的localVariable2
拷貝。然而,兩個localVariable2
的不一樣拷貝都指向堆上的同一個對象。代碼中經過一個靜態變量設置localVariable2
指向一個對象引用。僅存在一個靜態變量的一份拷貝,這份拷貝存放在堆上。所以,localVariable2
的兩份拷貝都指向由MySharedObject
指向的靜態變量的同一個實例。MySharedObject
實例也存放在堆上。它對應於上圖中的Object3。
注意,MySharedObject
類也包含兩個成員變量。這些成員變量隨着這個對象存放在堆上。這兩個成員變量指向另外兩個Integer
對象。這些Integer
對象對應於上圖中的Object2和Object4.
注意,methodTwo()
建立一個名爲localVariable
的本地變量。這個成員變量是一個指向一個Integer
對象的對象引用。這個方法設置localVariable1
引用指向一個新的Integer
實例。在執行methodTwo
方法時,localVariable1
引用將會在每一個線程中存放一份拷貝。這兩個Integer
對象實例化將會被存儲堆上,可是每次執行這個方法時,這個方法都會建立一個新的Integer
對象,兩個線程執行這個方法將會建立兩個不一樣的Integer
實例。methodTwo
方法建立的Integer
對象對應於上圖中的Object1和Object5。
還有一點,MySharedObject
類中的兩個long
類型的成員變量是原始類型的。由於,這些變量是成員變量,因此它們任然隨着該對象存放在堆上,僅有本地變量存放在線程棧上。
現代硬件內存模型與Java內存模型有一些不一樣。理解內存模型架構以及Java內存模型如何與它協同工做也是很是重要的。這部分描述了通用的硬件內存架構,下面的部分將會描述Java內存是如何與它「聯手」工做的。
下面是現代計算機硬件架構的簡單圖示:
一個現代計算機一般由兩個或者多個CPU。其中一些CPU還有多核。從這一點能夠看出,在一個有兩個或者多個CPU的現代計算機上同時運行多個線程是可能的。每一個CPU在某一時刻運行一個線程是沒有問題的。這意味着,若是你的Java程序是多線程的,在你的Java程序中每一個CPU上一個線程可能同時(併發)執行。
每一個CPU都包含一系列的寄存器,它們是CPU內內存的基礎。CPU在寄存器上執行操做的速度遠大於在主存上執行的速度。這是由於CPU訪問寄存器的速度遠大於主存。
每一個CPU可能還有一個CPU緩存層。實際上,絕大多數的現代CPU都有必定大小的緩存層。CPU訪問緩存層的速度快於訪問主存的速度,但一般比訪問內部寄存器的速度還要慢一點。一些CPU還有多層緩存,但這些對理解Java內存模型如何和內存交互不是那麼重要。只要知道CPU中能夠有一個緩存層就能夠了。
一個計算機還包含一個主存。全部的CPU均可以訪問主存。主存一般比CPU中的緩存大得多。
一般狀況下,當一個CPU須要讀取主存時,它會將主存的部分讀到CPU緩存中。它甚至可能將緩存中的部份內容讀到它的內部寄存器中,而後在寄存器中執行操做。當CPU須要將結果寫回到主存中去時,它會將內部寄存器的值刷新到緩存中,而後在某個時間點將值刷新回主存。
當CPU須要在緩存層存放一些東西的時候,存放在緩存中的內容一般會被刷新回主存。CPU緩存能夠在某一時刻將數據局部寫到它的內存中,和在某一時刻局部刷新它的內存。它不會再某一時刻讀/寫整個緩存。一般,在一個被稱做「cache lines」的更小的內存塊中緩存被更新。一個或者多個緩存行可能被讀到緩存,一個或者多個緩存行可能再被刷新回主存。
上面已經提到,Java內存模型與硬件內存架構之間存在差別。硬件內存架構沒有區分線程棧和堆。對於硬件,全部的線程棧和堆都分佈在主內中。部分線程棧和堆可能有時候會出如今CPU緩存中和CPU內部的寄存器中。以下圖所示:
當對象和變量被存放在計算機中各類不一樣的內存區域中時,就可能會出現一些具體的問題。主要包括以下兩個方面:
下面咱們專門來解釋如下這兩個問題。
若是兩個或者更多的線程在沒有正確的使用Volatile
聲明或者同步的狀況下共享一個對象,一個線程更新這個共享對象可能對其它線程來講是不接見的。
想象一下,共享對象被初始化在主存中。跑在CPU上的一個線程將這個共享對象讀到CPU緩存中。而後修改了這個對象。只要CPU緩存沒有被刷新會主存,對象修改後的版本對跑在其它CPU上的線程都是不可見的。這種方式可能致使每一個線程擁有這個共享對象的私有拷貝,每一個拷貝停留在不一樣的CPU緩存中。
下圖示意了這種情形。跑在左邊CPU的線程拷貝這個共享對象到它的CPU緩存中,而後將count變量的值修改成2。這個修改對跑在右邊CPU上的其它線程是不可見的,由於修改後的count的值尚未被刷新回主存中去。
解決這個問題你可使用Java中的volatile
關鍵字。volatile
關鍵字能夠保證直接從主存中讀取一個變量,若是這個變量被修改後,老是會被寫回到主存中去。
若是兩個或者更多的線程共享一個對象,多個線程在這個共享對象上更新變量,就有可能發生race conditions。
想象一下,若是線程A讀一個共享對象的變量count到它的CPU緩存中。再想象一下,線程B也作了一樣的事情,可是往一個不一樣的CPU緩存中。如今線程A將count
加1,線程B也作了一樣的事情。如今count
已經被增在了兩個,每一個CPU緩存中一次。
若是這些增長操做被順序的執行,變量count
應該被增長兩次,而後原值+2被寫回到主存中去。
然而,兩次增長都是在沒有適當的同步下併發執行的。不管是線程A仍是線程B將count
修改後的版本寫回到主存中取,修改後的值僅會被原值大1,儘管增長了兩次。
下圖演示了上面描述的狀況:
解決這個問題可使用Java同步塊。一個同步塊能夠保證在同一時刻僅有一個線程能夠進入代碼的臨界區。同步塊還能夠保證代碼塊中全部被訪問的變量將會從主存中讀入,當線程退出同步代碼塊時,全部被更新的變量都會被刷新回主存中去,無論這個變量是否被聲明爲volatile