Java內(nèi)存模型與硬件內(nèi)存架構(gòu)關(guān)系

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)用棧秒啦、線程棧上的局部變量以及堆上的對象:


image

一個局部變量可能是原始類型熬粗,這種情況它完全保存在線程棧上。

一個局部變量也可能是指向一個對象的引用余境。這種情況下驻呐,這個引用(局部變量)存儲在線程棧,但對象本身存儲在堆上芳来。

一個對象可能包含多個方法并且每個方法可能都有局部變量含末。這些局部變量同樣存儲在線程棧上,盡管這個方法所屬的對象保存的堆上即舌。

一個對象的成員變量與對象一起存儲在堆上佣盒。不管這個成員變量是原始類型還是對象的引用。

靜態(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緩存和寄存器中。如下圖:


image

當對象與變量能存儲在計算機的不同內(nèi)存區(qū)域時钱贯,某些問題可能會發(fā)生挫掏。主要兩個問題是:

  • 線程對共享變量的更新(寫)的可見性
  • 讀取、檢查與寫入共享變量時的競態(tài)條件

共享變量的可見性

兩個或多個線程正在共享一個對象秩命,如果沒有正確volatile聲明或同步砍濒,一個線程對共享變量的更新可能對另一線程是不可見的。

想象一下硫麻,共享變量開始存儲在主內(nèi)存中爸邢。運行在CPU 1上線程讀取了共享變量到CPU緩存中。然后它對共享變量進行了更改拿愧。只要CPU緩存沒有刷新回主內(nèi)存中杠河,共享變量的變更版本對運行在其他CPU上的線程是不可見的。


image

解決這個問題的辦法是你可以使用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。

下圖展示了上面的描述:

image

為了解決這個問題示辈,你可以使用Java同步塊寥茫。一個同步塊保證在給定時間內(nèi)只有一個線程能夠進入代碼的臨界區(qū)。同步塊也保證同步塊中訪問的所有變量會從主內(nèi)存讀取顽耳,并且離開同步塊后坠敷,所有更新的變量會刷新到主內(nèi)存中妙同,不管變量是否聲明為volatile射富。

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市粥帚,隨后出現(xiàn)的幾起案子胰耗,更是在濱河造成了極大的恐慌,老刑警劉巖芒涡,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件柴灯,死亡現(xiàn)場離奇詭異,居然都是意外死亡费尽,警方通過查閱死者的電腦和手機赠群,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來旱幼,“玉大人查描,你說我怎么就攤上這事“芈保” “怎么了冬三?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長缘缚。 經(jīng)常有香客問我勾笆,道長,這世上最難降的妖魔是什么桥滨? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任窝爪,我火速辦了婚禮,結(jié)果婚禮上齐媒,老公的妹妹穿的比我還像新娘酸舍。我一直安慰自己,他們只是感情好里初,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布啃勉。 她就那樣靜靜地躺著,像睡著了一般双妨。 火紅的嫁衣襯著肌膚如雪淮阐。 梳的紋絲不亂的頭發(fā)上叮阅,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機與錄音泣特,去河邊找鬼浩姥。 笑死,一個胖子當著我的面吹牛状您,可吹牛的內(nèi)容都是我干的勒叠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼膏孟,長吁一口氣:“原來是場噩夢啊……” “哼眯分!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起柒桑,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤弊决,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后魁淳,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體飘诗,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年界逛,在試婚紗的時候發(fā)現(xiàn)自己被綠了昆稿。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡息拜,死狀恐怖溉潭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情该溯,我是刑警寧澤岛抄,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站狈茉,受9級特大地震影響夫椭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜氯庆,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一蹭秋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧堤撵,春花似錦仁讨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春丈挟,著一層夾襖步出監(jiān)牢的瞬間刁卜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工曙咽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蛔趴,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓例朱,卻偏偏與公主長得像孝情,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子洒嗤,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容