Java內(nèi)存模型定義了Java虛擬機如何與計算機內(nèi)存進行交互。Java虛擬機是一個完整的計算機模型朗和,所以自然也包含內(nèi)存模型错沽,也就是Java內(nèi)存模型。
如果你想要正確地設計并發(fā)程序眶拉,了解Java內(nèi)存模型非常重要千埃。Java內(nèi)存模型定義了不同的線程如何以及何時能夠看見被其他線程寫入的變量,以及如何同步訪問共享變量忆植。
原始的Java內(nèi)存并不充分放可,所以Java 1.5 對Java內(nèi)存模型進行了修正谒臼。這個版本在Java 8中依然在使用。
內(nèi)部的Java內(nèi)存模型
JVM內(nèi)部使用的內(nèi)存模型劃分內(nèi)存為線程棧與堆耀里。
Java虛擬機中的每個線程有它自己的線程棧蜈缤。線程棧包含到當前執(zhí)行點前所有調(diào)用方法的信息。我把它稱為“調(diào)用椃肟妫”底哥。當線程執(zhí)行它的代碼時,調(diào)用棧就會發(fā)生變化房官。
線程棧也包含每個執(zhí)行方法的局部變量叠艳。一個線程只能訪問它自己的線程棧。線程創(chuàng)建的局部變量對其他所有線程是不可見的易阳。即使兩個線程執(zhí)行兩個完全相同的代碼附较,他們?nèi)匀粫谧约旱木€程棧上創(chuàng)建對應的局部變量。所以潦俺,每個線程對每個局部變量都有它自己的版本拒课。
所有原始類型的局部變量( boolean, byte, short, char, int, long, float, double) 完全存儲在線程棧,因此對其他線程不可見事示。一個線程可能會拷貝一份原始變量到另一個線程早像,但它不能共享那個原始的局部變量本身。
堆包含了Java應用程度創(chuàng)建的所有對象肖爵,不管是什么線程創(chuàng)建的卢鹦。這包含原始類型的包裝類(例如, Byte, Integer, Long等)。一個對象作為一個局部變量被創(chuàng)建劝堪,還是作為另一個對象的成員變量被創(chuàng)建都不重要冀自,這個對象仍然保存在堆上。
下圖闡述了調(diào)用棧秒啦、線程棧上的局部變量以及堆上的對象:
一個局部變量可能是原始類型熬粗,這種情況它完全保存在線程棧上。
一個局部變量也可能是指向一個對象的引用余境。這種情況下驻呐,這個引用(局部變量)存儲在線程棧,但對象本身存儲在堆上芳来。
一個對象可能包含多個方法并且每個方法可能都有局部變量含末。這些局部變量同樣存儲在線程棧上,盡管這個方法所屬的對象保存的堆上即舌。
一個對象的成員變量與對象一起存儲在堆上佣盒。不管這個成員變量是原始類型還是對象的引用。
靜態(tài)類變量與類定義一起存儲在堆上侥涵。
堆上的對象能被所有擁有這個對象的引用的線程訪問沼撕。當一個線程訪問一個對象時,它能夠訪問這個對象的成員變量芜飘。如果兩個線程同時訪問同一個對象务豺,它們都可能訪問這個對象的成員變量,但每個線程對每個變量有它自己的一份拷貝嗦明。
所以笼沥,什么樣的Java代碼才能導致如上圖的內(nèi)存布局,很簡單娶牌,就如以下代碼:
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;
}
硬件內(nèi)存架構(gòu)
現(xiàn)代硬件內(nèi)存架構(gòu)稍微與內(nèi)部Java內(nèi)存模塊有點不同奔浅。為了理解Java內(nèi)存模型,理解硬件內(nèi)存架構(gòu)很重要诗良。這節(jié)會介紹通用的硬件內(nèi)存架構(gòu)汹桦,后面的章節(jié)會介紹Java內(nèi)存模型如何與之工作。
以下是現(xiàn)代硬件內(nèi)存架構(gòu)的簡圖:
一臺現(xiàn)代計算機通常擁有2個或多個CPU鉴裹。這些CPU也可能有多個核舞骆。重點是,擁有2個或多個CPU的計算機可以同時運行多個線程径荔。在給定的時間內(nèi)督禽,每個CPU能夠運行一個線程。也就是說总处,如果你的Java應用程序是多線程的狈惫,多個線程會同時(或并發(fā)地)運行。
每個CPU包含一組寄存器鹦马,CPU訪問寄存器的速度遠大于訪問內(nèi)存胧谈。
同樣注意的是,第個CPU也會有緩存層的存在荸频。CPU訪問緩存的速度比訪問內(nèi)存要快第岖,但比訪問寄存器慢。所以试溯,CPU緩存速度介于寄存器與內(nèi)存之間蔑滓。
一臺計算機也包含主存區(qū)域(RAM),所有CPU都能夠訪問主存遇绞。主存區(qū)域要比緩存的容量要大得多键袱。
通常,當CPU訪問主存時摹闽,它會讀取主存的部分數(shù)據(jù)在緩存中蹄咖,甚至會讀取緩存中的部分數(shù)據(jù)到內(nèi)部的寄存器中,然后做運算付鹿。當CPU需要寫回主存時澜汤,它會將寄存器的數(shù)據(jù)刷新到緩存蚜迅,在某個時間點再刷新在主存。
當CPU需要在緩存中存儲其他數(shù)據(jù)時俊抵,此時緩存的數(shù)據(jù)通常會刷新回主存谁不。緩存以“緩存行”為單位。
Java內(nèi)存模型與硬件內(nèi)存架構(gòu)間的映射
正如已經(jīng)提到的徽诲,Java內(nèi)存模型與硬件內(nèi)存架構(gòu)是不同的刹帕。硬件內(nèi)存架構(gòu)不區(qū)分線程棧與堆。硬件上谎替,線程棧與堆都位于主內(nèi)存中偷溺。線程棧與堆的一部分可能有時會出現(xiàn)在CPU緩存和寄存器中。如下圖:
當對象與變量能存儲在計算機的不同內(nèi)存區(qū)域時钱贯,某些問題可能會發(fā)生挫掏。主要兩個問題是:
- 線程對共享變量的更新(寫)的可見性
- 讀取、檢查與寫入共享變量時的競態(tài)條件
共享變量的可見性
兩個或多個線程正在共享一個對象秩命,如果沒有正確volatile
聲明或同步砍濒,一個線程對共享變量的更新可能對另一線程是不可見的。
想象一下硫麻,共享變量開始存儲在主內(nèi)存中爸邢。運行在CPU 1上線程讀取了共享變量到CPU緩存中。然后它對共享變量進行了更改拿愧。只要CPU緩存沒有刷新回主內(nèi)存中杠河,共享變量的變更版本對運行在其他CPU上的線程是不可見的。
解決這個問題的辦法是你可以使用Java關(guān)鍵字volatile
浇辜。volatile
關(guān)鍵字能夠保證一個給定的變量從主內(nèi)存中直接讀取券敌,并且更新時總是寫回到主內(nèi)存。
競態(tài)條件
如果兩個或多個線程共享一個對象柳洋,并且超過一個線程更新共享對象中的變量待诅,競態(tài)條件可能發(fā)生。
想象一下熊镣,如果線程A讀取共享對象的變量 count
到它的CPU緩存卑雁。同樣,線程B也做相同的操作绪囱,但是是不同的CPU緩存测蹲。線程A和線程B都對count
加1。現(xiàn)在count
被增加了兩次鬼吵。
如果這些增加操作是按順序執(zhí)行的扣甲,變量count
應該增加了兩次并且以原有值+2為新值寫回到主內(nèi)存。
然而齿椅,兩次增加操作是沒有同步的并發(fā)操作琉挖。不然是線程A還是線程B將更新后的count
寫回到主內(nèi)存启泣,更新值僅僅比原來的值大1。
下圖展示了上面的描述:
為了解決這個問題示辈,你可以使用Java同步塊寥茫。一個同步塊保證在給定時間內(nèi)只有一個線程能夠進入代碼的臨界區(qū)。同步塊也保證同步塊中訪問的所有變量會從主內(nèi)存讀取顽耳,并且離開同步塊后坠敷,所有更新的變量會刷新到主內(nèi)存中妙同,不管變量是否聲明為
volatile
射富。