【轉(zhuǎn)】Java 內(nèi)存模型

轉(zhuǎn)自Java 內(nèi)存模型

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

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

原始的 Java 內(nèi)存模型存在一些不足亡鼠,因此 Java 內(nèi)存模型在 Java1.5 時被重新修訂赏殃。這個版本的 Java 內(nèi)存模型在 Java8 中人在使用。

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

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

每一個運行在 Java 虛擬機里的線程都擁有自己的線程棧。這個線程棧包含了這個線程調(diào)用的方法當(dāng)前執(zhí)行點相關(guān)的信息浑厚。一個線程僅能訪問自己的線程棧股耽。一個線程創(chuàng)建的本地變量對其它線程不可見,僅自己可見钳幅。即使兩個線程執(zhí)行同樣的代碼物蝙,這兩個線程仍然在自己的線程棧中的代碼來創(chuàng)建本地變量。因此敢艰,每個線程擁有每個本地變量的獨有版本诬乞。

所有原始類型的本地變量都存放在線程棧上,因此對其它線程不可見钠导。一個線程可能向另一個線程傳遞一個原始類型變量的拷貝震嫉,但是它不能共享這個原始類型變量自身。

堆上包含在 Java 程序中創(chuàng)建的所有對象牡属,無論是哪一個對象創(chuàng)建的票堵。這包括原始類型的對象版本。如果一個對象被創(chuàng)建然后賦值給一個局部變量逮栅,或者用來作為另一個對象的成員變量悴势,這個對象仍然是存放在堆上。

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

一個本地變量可能是原始類型,在這種情況下侥加,它總是“呆在”線程棧上捧存。
一個本地變量也可能是指向一個對象的一個引用。在這種情況下,引用(這個本地變量)存放在線程棧上昔穴,但是對象本身存放在堆上短蜕。
一個對象可能包含方法,這些方法可能包含本地變量傻咖。這些本地變量任然存放在線程棧上,即使這些方法所屬的對象存放在堆上岖研。
一個對象的成員變量會隨著這個對象自身存放在堆上卿操。不管這個成員變量是原始類型還是引用類型
靜態(tài)成員變量跟隨著類定義一起也存放在堆上孙援。

存放在堆上的對象可以被所有持有這個對象引用的線程訪問害淤。當(dāng)一個線程可以訪問一個對象時,它也可以訪問這個對象的成員變量尔邓。如果兩個線程同時調(diào)用同一個對象上的同一個方法地熄,它們將都會訪問這個對象的本地變量另假,但是每一個線程都擁有這個本地變量的私有拷貝。

下圖演示了上面提到的點:

兩個線程擁有一些如上圖列出的本地變量崭放。其中一個本地變量(Local Variable 2)指向堆上的一個共享對象(Object 3)。這兩個線程分別擁有同一個對象的不同引用鸽凶。這些引用都是本地變量币砂,因此存放在各自線程的線程棧上。這兩個不同的引用指向堆上同一個對象玻侥。

注意决摧,這個共享對象(Object 3)持有 Object2Object4 的引用作為其成員變量(如圖中 Object3 指向 Object2Object4 的箭頭)。通過在 Object3 中這些成員變量引用凑兰,這兩個線程就可以訪問 Object2Object4掌桩。

這張圖也展示了指向堆上兩個不同對象的一個本地變量。在這種情況下姑食,指向兩個不同對象的引用不是同一個對象波岛。理論上,兩個線程都可以訪問 Object1Object5音半,如果兩個線程都擁有兩個對象的引用盆色。但是在上圖中,每一個線程僅有一個引用指向兩個對象其中之一祟剔。

因此隔躲,什么類型的 Java 代碼會導(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;
}

如果兩個線程同時執(zhí)行 run() 方法物延,就會出現(xiàn)上圖所示的情景宣旱。run() 方法調(diào)用 methodOne() 方法,methodOne() 調(diào)用 methodTwo() 方法叛薯。

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

每個線程執(zhí)行 methodOne() 都會在它們對應(yīng)的線程棧上創(chuàng)建 localVariable1localVariable2 的私有拷貝笙纤。localVariable1 變量彼此完全獨立,僅“生活”在每個線程的線程棧上组力。一個線程看不到另一個線程對它的 localVariable1 私有拷貝做出的修改省容。

每個線程執(zhí)行 methodOne() 時也將會創(chuàng)建它們各自的 localVariable2 拷貝。然而燎字,兩個 localVariable2 的不同拷貝都指向堆上的同一個對象腥椒。代碼中通過一個靜態(tài)變量設(shè)置 localVariable2 指向一個對象引用。既僅存在一個靜態(tài)變量的一份拷貝候衍,這份拷貝存放在堆上笼蛛。因此,localVariable2 的兩份拷貝都指向由 MySharedObject 指向的靜態(tài)變量的同一個實例蛉鹿。MySharedObject 實例也存放在堆上滨砍。它對應(yīng)于上圖中的 Object3

注意妖异,MySharedObject 類也包含兩個成員變量惋戏。這些成員變量隨著這個對象存放在堆上。這兩個成員變量指向另外兩個 Integer 對象他膳。這些 Integer 對象對應(yīng)于上圖中的 Object2 和 Object4.

注意日川,methodTwo() 創(chuàng)建一個名為 localVariable1 的本地變量。這個成員變量是一個指向一個 Integer 對象的對象引用矩乐。這個方法設(shè)置 localVariable1 引用指向一個新的 Integer 實例龄句。在執(zhí)行 methodTwo() 方法時,localVariable1 引用將會在每個線程中存放一份拷貝散罕。這兩個 Integer 對象實例化將會被存儲堆上分歇,但是每次執(zhí)行這個方法時,這個方法都會創(chuàng)建一個新的 Integer 對象欧漱,兩個線程執(zhí)行這個方法將會創(chuàng)建兩個不同的 Integer 實例职抡。methodTwo 方法創(chuàng)建的 Integer 對象對應(yīng)于上圖中的 Object1Object5

注意 MySharedObject 類中的兩個 long 類型的成員變量是原始類型的误甚。因為缚甩,這些變量是成員變量,所以它們?nèi)匀浑S著該對象存放在堆上窑邦,僅有本地變量存放在線程棧上擅威。

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

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

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

一個現(xiàn)代計算機通常由兩個或者多個 CPU。其中一些 CPU 還有多核厉熟。從這一點可以看出导盅,在一個有兩個或者多個 CPU 的現(xiàn)代計算機上同時運行多個線程是可能的。每個 CPU 在某一時刻運行一個線程是沒有問題的揍瑟。這意味著白翻,如果你的 Java 程序是多線程的,在你的 Java 程序中每個 CPU 上一個線程可能同時(并發(fā))執(zhí)行绢片。

每個 CPU 都包含一系列的寄存器滤馍,它們是 CPU 內(nèi)內(nèi)存的基礎(chǔ)。CPU 在寄存器上執(zhí)行操作的速度遠(yuǎn)大于在主存上執(zhí)行的速度杉畜。這是因為 CPU 訪問寄存器的速度遠(yuǎn)大于主存。

每個 CPU 可能還有一個 CPU 緩存層衷恭。實際上此叠,絕大多數(shù)的現(xiàn)代 CPU 都有一定大小的緩存層。CPU 訪問緩存層的速度快于訪問主存的速度随珠,但通常比訪問內(nèi)部寄存器的速度還要慢一點灭袁。一些 CPU 還有多層緩存,但這些對理解 Java 內(nèi)存模型如何和內(nèi)存交互不是那么重要窗看。只要知道 CPU 中可以有一個緩存層就可以了茸歧。

一個計算機還包含一個主存。所有的 CPU 都可以訪問主存显沈。主存通常比 CPU 中的緩存大得多软瞎。

通常情況下,當(dāng)一個 CPU 需要讀取主存時拉讯,它會將主存的部分讀到 CPU 緩存中涤浇。它甚至可能將緩存中的部分內(nèi)容讀到它的內(nèi)部寄存器中,然后在寄存器中執(zhí)行操作魔慷。當(dāng) CPU 需要將結(jié)果寫回到主存中去時只锭,它會將內(nèi)部寄存器的值刷新到緩存中,然后在某個時間點將值刷新回主存院尔。

當(dāng) CPU 需要在緩存層存放一些東西的時候蜻展,存放在緩存中的內(nèi)容通常會被刷新回主存。CPU 緩存可在某一時刻將數(shù)據(jù)局部寫到它的內(nèi)存中邀摆,在某一時刻局部刷新它的內(nèi)存纵顾。它不會在某一時刻讀/寫整個緩存。通常栋盹,在一個被稱作 “cache lines” 的更小內(nèi)存塊中緩存被更新片挂。一個或者多個緩存行可能被讀到緩存,一個或者多個緩存行可能再被刷新回主存。

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 緩存中和 CPU 內(nèi)部的寄存器中。如下圖所示:

當(dāng)對象和變量被存放在計算機中各種不同的內(nèi)存區(qū)域中時讥脐,就可能會出現(xiàn)一些具體的問題遭居。主要包括如下兩個方面:

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

下面我們專門來解釋以下這兩個問題旬渠。

共享對象可見性

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

想象一下告丢,共享對象被初始化在主存中枪蘑。跑在 CPU 上的一個線程將這個共享對象讀到 CPU 緩存中。然后修改了這個對象岖免。只要 CPU 緩存沒有被刷新會主存岳颇,對象修改后的版本對跑在其它 CPU 上的線程都是不可見的。這種方式可能導(dǎo)致每個線程擁有這個共享對象的私有拷貝颅湘,每個拷貝停留在不同的 CPU 緩存中话侧。

下圖示意了這種情形。跑在左邊 CPU 的線程拷貝這個共享對象到它的 CPU 緩存中闯参,然后將 count 變量的值修改為 2瞻鹏。這個修改對跑在右邊 CPU 上的其它線程是不可見的,因為修改后的 count 的值還沒有被刷新回主存中去鹿寨。

解決這個問題你可以使用 Java 中的 volatile 關(guān)鍵字乙漓。volatile 關(guān)鍵字可以保證直接從主存中讀取一個變量,如果這個變量被修改后释移,總是會被寫回到主存中去叭披。

Race Conditions

如果兩個或者更多的線程共享一個對象,多個線程在這個共享對象上更新變量玩讳,就有可能發(fā)生 race conditions涩蜘。

想象一下,如果線程 A 讀一個共享對象的變量 count 到它的 CPU 緩存中熏纯。再想象一下同诫,線程 B 也做了同樣的事情,但是往一個不同的 CPU 緩存中≌晾剑現(xiàn)在線程 A 將 count 加 1误窖,線程 B 也做了同樣的事情《E蹋現(xiàn)在 count 已經(jīng)被增在了兩個,每個 CPU 緩存中一次霹俺。

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

然而愈魏,兩次增加都是在沒有適當(dāng)?shù)耐较虏l(fā)執(zhí)行的。無論是線程 A 還是線程 B 將 count 修改后的版本寫回到主存中取想际,修改后的值僅會被原值大 1培漏,盡管增加了兩次。
下圖演示了上面描述的情況:

解決這個問題可以使用 Java 同步塊胡本。一個同步塊可以保證在同一時刻僅有一個線程可以進(jìn)入代碼的臨界區(qū)牌柄。同步塊還可以保證代碼塊中所有被訪問的變量將會從主存中讀入,當(dāng)線程退出同步代碼塊時侧甫,所有被更新的變量都會被刷新回主存中去珊佣,不管這個變量是否被聲明為 volatile。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末闺骚,一起剝皮案震驚了整個濱河市彩扔,隨后出現(xiàn)的幾起案子妆档,更是在濱河造成了極大的恐慌僻爽,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贾惦,死亡現(xiàn)場離奇詭異胸梆,居然都是意外死亡,警方通過查閱死者的電腦和手機须板,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門碰镜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人习瑰,你說我怎么就攤上這事绪颖。” “怎么了甜奄?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵柠横,是天一觀的道長。 經(jīng)常有香客問我课兄,道長牍氛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任烟阐,我火速辦了婚禮搬俊,結(jié)果婚禮上紊扬,老公的妹妹穿的比我還像新娘。我一直安慰自己唉擂,他們只是感情好餐屎,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著楔敌,像睡著了一般啤挎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上卵凑,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天庆聘,我揣著相機與錄音,去河邊找鬼勺卢。 笑死伙判,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的黑忱。 我是一名探鬼主播宴抚,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼甫煞!你這毒婦竟也來了菇曲?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤抚吠,失蹤者是張志新(化名)和其女友劉穎常潮,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體楷力,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡喊式,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了萧朝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片岔留。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖检柬,靈堂內(nèi)的尸體忽然破棺而出献联,到底是詐尸還是另有隱情,我是刑警寧澤何址,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布里逆,位于F島的核電站,受9級特大地震影響头朱,放射性物質(zhì)發(fā)生泄漏运悲。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一项钮、第九天 我趴在偏房一處隱蔽的房頂上張望班眯。 院中可真熱鬧希停,春花似錦、人聲如沸署隘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽磁餐。三九已至违崇,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間诊霹,已是汗流浹背羞延。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留脾还,地道東北人伴箩。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像鄙漏,于是被迫代替她去往敵國和親嗤谚。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345

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

  • 從三月份找實習(xí)到現(xiàn)在怔蚌,面了一些公司巩步,掛了不少,但最終還是拿到小米桦踊、百度椅野、阿里、京東钞钙、新浪鳄橘、CVTE声离、樂視家的研發(fā)崗...
    時芥藍(lán)閱讀 42,192評論 11 349
  • JVM內(nèi)存模型Java虛擬機(Java Virtual Machine=JVM)的內(nèi)存空間分為五個部分芒炼,分別是: ...
    光劍書架上的書閱讀 2,483評論 2 26
  • Java8張圖 11、字符串不變性 12术徊、equals()方法本刽、hashCode()方法的區(qū)別 13、...
    Miley_MOJIE閱讀 3,693評論 0 11
  • 2017年5月28日早上七點一刻赠涮,我們一行七人到了西寧站子寓,出了車站,跟我們的領(lǐng)隊接上頭笋除,等了大概半小時斜友,全員到齊之...
    青槐兒閱讀 165評論 0 0
  • chater2流年 陸幽,從沒想過垃它,會在這種情況下看見被她默默叫做“小白”的白子孑鲜屏,不知道她是怎么回到家的烹看。只是腦...
    Alma閱讀 471評論 0 1