Java內(nèi)存模型

英文原文:Java Memory Model
作者:Jakob Jenkov
譯者:張坤
原文地址:http://ifeve.com/java-memory-model-6/

Java內(nèi)存模型規(guī)范了Java虛擬機(jī)與計(jì)算機(jī)內(nèi)存是如何協(xié)同工作的兼呵。Java虛擬機(jī)是一個(gè)完整的計(jì)算機(jī)的一個(gè)模型,因此這個(gè)模型自然也包含一個(gè)內(nèi)存模型——又稱為Java內(nèi)存模型储狭。

如果你想設(shè)計(jì)表現(xiàn)良好的并發(fā)程序殿托,理解Java內(nèi)存模型是非常重要的磷瘤。Java內(nèi)存模型規(guī)定了如何和何時(shí)可以看到由其他線程修改過后的共享變量的值,以及在必須時(shí)如何同步的訪問共享變量。

原始的Java內(nèi)存模型存在一些不足筑悴,因此Java內(nèi)存模型在Java1.5時(shí)被重新修訂谁鳍。這個(gè)版本的Java內(nèi)存模型在Java8中人在使用癞季。

Java內(nèi)存模型內(nèi)部原理

Java內(nèi)存模型把Java虛擬機(jī)內(nèi)部劃分為線程棧和堆。這張圖演示了Java內(nèi)存模型的邏輯視圖棠耕。

Java Memory Model

每一個(gè)運(yùn)行在Java虛擬機(jī)里的線程都擁有自己的線程棧余佛。這個(gè)線程棧包含了這個(gè)線程調(diào)用的方法當(dāng)前執(zhí)行點(diǎn)相關(guān)的信息。一個(gè)線程僅能訪問自己的線程棧窍荧。一個(gè)線程創(chuàng)建的本地變量對(duì)其它線程不可見辉巡,僅自己可見。即使兩個(gè)線程執(zhí)行同樣的代碼蕊退,這兩個(gè)線程任然在在自己的線程棧中的代碼來創(chuàng)建本地變量郊楣。因此,每個(gè)線程擁有每個(gè)本地變量的獨(dú)有版本瓤荔。

所有原始類型的本地變量都存放在線程棧上净蚤,因此對(duì)其它線程不可見。一個(gè)線程可能向另一個(gè)線程傳遞一個(gè)原始類型變量的拷貝输硝,但是它不能共享這個(gè)原始類型變量自身今瀑。

堆上包含在Java程序中創(chuàng)建的所有對(duì)象,無論是哪一個(gè)對(duì)象創(chuàng)建的点把。這包括原始類型的對(duì)象版本橘荠。如果一個(gè)對(duì)象被創(chuàng)建然后賦值給一個(gè)局部變量,或者用來作為另一個(gè)對(duì)象的成員變量郎逃,這個(gè)對(duì)象任然是存放在堆上哥童。

下面這張圖演示了調(diào)用棧和本地變量存放在線程棧上,對(duì)象存放在堆上褒翰。

  • 一個(gè)本地變量可能是原始類型贮懈,在這種情況下匀泊,它總是“呆在”線程棧上。
  • 一個(gè)本地變量也可能是指向一個(gè)對(duì)象的一個(gè)引用朵你。在這種情況下各聘,引用(這個(gè)本地變量)存放在線程棧上,但是對(duì)象本身存放在堆上撬呢。
  • 一個(gè)對(duì)象可能包含方法伦吠,這些方法可能包含本地變量。這些本地變量任然存放在線程棧上魂拦,即使這些方法所屬的對(duì)象存放在堆上毛仪。
  • 一個(gè)對(duì)象的成員變量可能隨著這個(gè)對(duì)象自身存放在堆上。不管這個(gè)成員變量是原始類型還是引用類型芯勘。

靜態(tài)成員變量跟隨著類定義一起也存放在堆上箱靴。

存放在堆上的對(duì)象可以被所有持有對(duì)這個(gè)對(duì)象引用的線程訪問。當(dāng)一個(gè)線程可以訪問一個(gè)對(duì)象時(shí)荷愕,它也可以訪問這個(gè)對(duì)象的成員變量衡怀。如果兩個(gè)線程同時(shí)調(diào)用同一個(gè)對(duì)象上的同一個(gè)方法,它們將會(huì)都訪問這個(gè)對(duì)象的成員變量安疗,但是每一個(gè)線程都擁有這個(gè)本地變量的私有拷貝抛杨。

下圖演示了上面提到的點(diǎn):

兩個(gè)線程擁有一些列的本地變量。其中一個(gè)本地變量(Local Variable 2)執(zhí)行堆上的一個(gè)共享對(duì)象(Object 3)荐类。這兩個(gè)線程分別擁有同一個(gè)對(duì)象的不同引用怖现。這些引用都是本地變量,因此存放在各自線程的線程棧上玉罐。這兩個(gè)不同的引用指向堆上同一個(gè)對(duì)象屈嗤。

注意,這個(gè)共享對(duì)象(Object 3)持有Object2和Object4一個(gè)引用作為其成員變量(如圖中Object3指向Object2和Object4的箭頭)吊输。通過在Object3中這些成員變量引用饶号,這兩個(gè)線程就可以訪問Object2和Object4。

這張圖也展示了指向堆上兩個(gè)不同對(duì)象的一個(gè)本地變量季蚂。在這種情況下茫船,指向兩個(gè)不同對(duì)象的引用不是同一個(gè)對(duì)象。理論上扭屁,兩個(gè)線程都可以訪問Object1和Object5透硝,如果兩個(gè)線程都擁有兩個(gè)對(duì)象的引用。但是在上圖中疯搅,每一個(gè)線程僅有一個(gè)引用指向兩個(gè)對(duì)象其中之一。

因此埋泵,什么類型的Java代碼會(huì)導(dǎo)致上面的內(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;
}

如果兩個(gè)線程同時(shí)執(zhí)行run()方法罪治,就會(huì)出現(xiàn)上圖所示的情景。run()方法調(diào)用methodOne()方法礁蔗,methodOne()調(diào)用methodTwo()方法觉义。

methodOne()聲明了一個(gè)原始類型的本地變量和一個(gè)引用類型的本地變量。

每個(gè)線程執(zhí)行methodOne()都會(huì)在它們對(duì)應(yīng)的線程棧上創(chuàng)建localVariable1localVariable2的私有拷貝浴井。localVariable1變量彼此完全獨(dú)立晒骇,僅“生活”在每個(gè)線程的線程棧上。一個(gè)線程看不到另一個(gè)線程對(duì)它的localVariable1私有拷貝做出的修改磺浙。

每個(gè)線程執(zhí)行methodOne()時(shí)也將會(huì)創(chuàng)建它們各自的localVariable2拷貝洪囤。然而,兩個(gè)localVariable2的不同拷貝都指向堆上的同一個(gè)對(duì)象撕氧。代碼中通過一個(gè)靜態(tài)變量設(shè)置localVariable2指向一個(gè)對(duì)象引用瘤缩。僅存在一個(gè)靜態(tài)變量的一份拷貝,這份拷貝存放在堆上伦泥。因此剥啤,localVariable2的兩份拷貝都指向由MySharedObject指向的靜態(tài)變量的同一個(gè)實(shí)例。MySharedObject實(shí)例也存放在堆上不脯。它對(duì)應(yīng)于上圖中的Object3府怯。

注意,MySharedObject類也包含兩個(gè)成員變量防楷。這些成員變量隨著這個(gè)對(duì)象存放在堆上牺丙。這兩個(gè)成員變量指向另外兩個(gè)Integer對(duì)象。這些Integer對(duì)象對(duì)應(yīng)于上圖中的Object2和Object4.

注意域帐,methodTwo()創(chuàng)建一個(gè)名為localVariable的本地變量赘被。這個(gè)成員變量是一個(gè)指向一個(gè)Integer對(duì)象的對(duì)象引用。這個(gè)方法設(shè)置localVariable1引用指向一個(gè)新的Integer實(shí)例肖揣。在執(zhí)行methodTwo方法時(shí)民假,localVariable1引用將會(huì)在每個(gè)線程中存放一份拷貝。這兩個(gè)Integer對(duì)象實(shí)例化將會(huì)被存儲(chǔ)堆上龙优,但是每次執(zhí)行這個(gè)方法時(shí)羊异,這個(gè)方法都會(huì)創(chuàng)建一個(gè)新的Integer對(duì)象,兩個(gè)線程執(zhí)行這個(gè)方法將會(huì)創(chuàng)建兩個(gè)不同的Integer實(shí)例彤断。methodTwo方法創(chuàng)建的Integer對(duì)象對(duì)應(yīng)于上圖中的Object1和Object5野舶。

還有一點(diǎn),MySharedObject類中的兩個(gè)long類型的成員變量是原始類型的宰衙。因?yàn)槠降溃@些變量是成員變量,所以它們?nèi)稳浑S著該對(duì)象存放在堆上供炼,僅有本地變量存放在線程棧上一屋。

硬件內(nèi)存架構(gòu)

現(xiàn)代硬件內(nèi)存模型與Java內(nèi)存模型有一些不同窘疮。理解內(nèi)存模型架構(gòu)以及Java內(nèi)存模型如何與它協(xié)同工作也是非常重要的。這部分描述了通用的硬件內(nèi)存架構(gòu)冀墨,下面的部分將會(huì)描述Java內(nèi)存是如何與它“聯(lián)手”工作的闸衫。

下面是現(xiàn)代計(jì)算機(jī)硬件架構(gòu)的簡單圖示:

一個(gè)現(xiàn)代計(jì)算機(jī)通常由兩個(gè)或者多個(gè)CPU。其中一些CPU還有多核诽嘉。從這一點(diǎn)可以看出蔚出,在一個(gè)有兩個(gè)或者多個(gè)CPU的現(xiàn)代計(jì)算機(jī)上同時(shí)運(yùn)行多個(gè)線程是可能的。每個(gè)CPU在某一時(shí)刻運(yùn)行一個(gè)線程是沒有問題的虫腋。這意味著骄酗,如果你的Java程序是多線程的,在你的Java程序中每個(gè)CPU上一個(gè)線程可能同時(shí)(并發(fā))執(zhí)行岔乔。

每個(gè)CPU都包含一系列的寄存器酥筝,它們是CPU內(nèi)內(nèi)存的基礎(chǔ)。CPU在寄存器上執(zhí)行操作的速度遠(yuǎn)大于在主存上執(zhí)行的速度雏门。這是因?yàn)镃PU訪問寄存器的速度遠(yuǎn)大于主存嘿歌。

每個(gè)CPU可能還有一個(gè)CPU緩存層。實(shí)際上茁影,絕大多數(shù)的現(xiàn)代CPU都有一定大小的緩存層宙帝。CPU訪問緩存層的速度快于訪問主存的速度,但通常比訪問內(nèi)部寄存器的速度還要慢一點(diǎn)募闲。一些CPU還有多層緩存步脓,但這些對(duì)理解Java內(nèi)存模型如何和內(nèi)存交互不是那么重要。只要知道CPU中可以有一個(gè)緩存層就可以了浩螺。

一個(gè)計(jì)算機(jī)還包含一個(gè)主存靴患。所有的CPU都可以訪問主存。主存通常比CPU中的緩存大得多要出。

通常情況下鸳君,當(dāng)一個(gè)CPU需要讀取主存時(shí),它會(huì)將主存的部分讀到CPU緩存中患蹂。它甚至可能將緩存中的部分內(nèi)容讀到它的內(nèi)部寄存器中或颊,然后在寄存器中執(zhí)行操作。當(dāng)CPU需要將結(jié)果寫回到主存中去時(shí)传于,它會(huì)將內(nèi)部寄存器的值刷新到緩存中囱挑,然后在某個(gè)時(shí)間點(diǎn)將值刷新回主存。

當(dāng)CPU需要在緩存層存放一些東西的時(shí)候沼溜,存放在緩存中的內(nèi)容通常會(huì)被刷新回主存平挑。CPU緩存可以在某一時(shí)刻將數(shù)據(jù)局部寫到它的內(nèi)存中,和在某一時(shí)刻局部刷新它的內(nèi)存系草。它不會(huì)再某一時(shí)刻讀/寫整個(gè)緩存弹惦。通常否淤,在一個(gè)被稱作“cache lines”的更小的內(nèi)存塊中緩存被更新。一個(gè)或者多個(gè)緩存行可能被讀到緩存棠隐,一個(gè)或者多個(gè)緩存行可能再被刷新回主存。

Java內(nèi)存模型和硬件內(nèi)存架構(gòu)之間的橋接

上面已經(jīng)提到檐嚣,Java內(nèi)存模型與硬件內(nèi)存架構(gòu)之間存在差異助泽。硬件內(nèi)存架構(gòu)沒有區(qū)分線程棧和堆。對(duì)于硬件嚎京,所有的線程棧和堆都分布在主內(nèi)中嗡贺。部分線程棧和堆可能有時(shí)候會(huì)出現(xiàn)在CPU緩存中和CPU內(nèi)部的寄存器中。如下圖所示:

當(dāng)對(duì)象和變量被存放在計(jì)算機(jī)中各種不同的內(nèi)存區(qū)域中時(shí)鞍帝,就可能會(huì)出現(xiàn)一些具體的問題诫睬。主要包括如下兩個(gè)方面:

  • 線程對(duì)共享變量修改的可見性
  • 當(dāng)讀,寫和檢查共享變量時(shí)出現(xiàn)race conditions

下面我們專門來解釋以下這兩個(gè)問題帕涌。

共享對(duì)象可見性

如果兩個(gè)或者更多的線程在沒有正確的使用volatile聲明或者同步的情況下共享一個(gè)對(duì)象摄凡,一個(gè)線程更新這個(gè)共享對(duì)象可能對(duì)其它線程來說是不接見的。

想象一下蚓曼,共享對(duì)象被初始化在主存中亲澡。跑在CPU上的一個(gè)線程將這個(gè)共享對(duì)象讀到CPU緩存中。然后修改了這個(gè)對(duì)象纫版。只要CPU緩存沒有被刷新會(huì)主存床绪,對(duì)象修改后的版本對(duì)跑在其它CPU上的線程都是不可見的。這種方式可能導(dǎo)致每個(gè)線程擁有這個(gè)共享對(duì)象的私有拷貝其弊,每個(gè)拷貝停留在不同的CPU緩存中癞己。

下圖示意了這種情形。跑在左邊CPU的線程拷貝這個(gè)共享對(duì)象到它的CPU緩存中梭伐,然后將count變量的值修改為2痹雅。這個(gè)修改對(duì)跑在右邊CPU上的其它線程是不可見的,因?yàn)樾薷暮蟮?code>count的值還沒有被刷新回主存中去籽御。

解決這個(gè)問題你可以使用Java中的volatile關(guān)鍵字练慕。volatile關(guān)鍵字可以保證直接從主存中讀取一個(gè)變量,如果這個(gè)變量被修改后技掏,總是會(huì)被寫回到主存中去铃将。

Race Conditions

如果兩個(gè)或者更多的線程共享一個(gè)對(duì)象,多個(gè)線程在這個(gè)共享對(duì)象上更新變量哑梳,就有可能發(fā)生race conditions劲阎。

想象一下,如果線程A讀一個(gè)共享對(duì)象的變量count到它的CPU緩存中鸠真。再想象一下悯仙,線程B也做了同樣的事情龄毡,但是往一個(gè)不同的CPU緩存中。現(xiàn)在線程Acount1锡垄,線程B也做了同樣的事情÷倭悖現(xiàn)在count已經(jīng)被增在了兩個(gè),每個(gè)CPU緩存中一次货岭。

如果這些增加操作被順序的執(zhí)行路操,變量count應(yīng)該被增加兩次,然后原值+2被寫回到主存中去千贯。

然而屯仗,兩次增加都是在沒有適當(dāng)?shù)耐较虏l(fā)執(zhí)行的。無論是線程A還是線程Bcount修改后的版本寫回到主存中取搔谴,修改后的值僅會(huì)被原值大1魁袜,盡管增加了兩次。

下圖演示了上面描述的情況:

解決這個(gè)問題可以使用Java同步塊敦第。一個(gè)同步塊可以保證在同一時(shí)刻僅有一個(gè)線程可以進(jìn)入代碼的臨界區(qū)峰弹。同步塊還可以保證代碼塊中所有被訪問的變量將會(huì)從主存中讀入,當(dāng)線程退出同步代碼塊時(shí)申尼,所有被更新的變量都會(huì)被刷新回主存中去垮卓,不管這個(gè)變量是否被聲明為volatile

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末师幕,一起剝皮案震驚了整個(gè)濱河市粟按,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌霹粥,老刑警劉巖灭将,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異后控,居然都是意外死亡庙曙,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門浩淘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捌朴,“玉大人,你說我怎么就攤上這事张抄∩氨危” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵署惯,是天一觀的道長左驾。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么诡右? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任安岂,我火速辦了婚禮,結(jié)果婚禮上帆吻,老公的妹妹穿的比我還像新娘域那。我一直安慰自己,他們只是感情好猜煮,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布琉雳。 她就那樣靜靜地躺著,像睡著了一般友瘤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上檐束,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天辫秧,我揣著相機(jī)與錄音,去河邊找鬼被丧。 笑死盟戏,一個(gè)胖子當(dāng)著我的面吹牛即纲,可吹牛的內(nèi)容都是我干的口蝠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼孤里,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼黄选!你這毒婦竟也來了蝇摸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤办陷,失蹤者是張志新(化名)和其女友劉穎貌夕,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體民镜,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡啡专,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了制圈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片们童。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖鲸鹦,靈堂內(nèi)的尸體忽然破棺而出慧库,到底是詐尸還是另有隱情,我是刑警寧澤亥鬓,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布完沪,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏覆积。R本人自食惡果不足惜听皿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望宽档。 院中可真熱鬧尉姨,春花似錦、人聲如沸吗冤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽椎瘟。三九已至覆致,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肺蔚,已是汗流浹背煌妈。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宣羊,地道東北人璧诵。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像仇冯,于是被迫代替她去往敵國和親之宿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

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

  • 轉(zhuǎn)自Java 內(nèi)存模型 Java 內(nèi)存模型規(guī)范了 Java 虛擬機(jī)與計(jì)算機(jī)內(nèi)存是如何協(xié)同工作的苛坚。Java 虛擬機(jī)是...
    騎摩托馬斯閱讀 1,274評(píng)論 0 5
  • Java內(nèi)存模型規(guī)范了Java虛擬機(jī)與計(jì)算機(jī)內(nèi)存是如何協(xié)同工作的比被。Java虛擬機(jī)是一個(gè)完整的計(jì)算機(jī)的一個(gè)模型,因此...
    wolfudog閱讀 206評(píng)論 0 0
  • 版權(quán)聲明:轉(zhuǎn)載于 http://blog.csdn.net/ccj659/article/details/5301...
    柒黍閱讀 547評(píng)論 0 5
  • 【帥哥和帥哥在一起才最搭炕婶,后續(xù)】 陽光姐赡,風(fēng)起。 發(fā)絲柠掂,輕動(dòng)项滑。 媚眼,帶笑涯贞。 便暗淡了周遭枪狂。 自此,連夢(mèng)里都是你宋渔。 ...
    慕獄閱讀 580評(píng)論 0 1
  • 一個(gè)人敢于直面人生真相的程度和決心州疾,決定著ta坦然面對(duì)日復(fù)日的時(shí)光的飽和度。 畢業(yè)兩年皇拣,思緒萬千必須先占個(gè)坑严蓖,等醞...
    愛吃甜食的考拉小姐閱讀 219評(píng)論 0 2