初識 JAVA 內(nèi)存模型:結(jié)合硬件內(nèi)存架構(gòu)理解 JAVA 內(nèi)存模型

寫在前面

JAVA 內(nèi)存模型是我看過很多遍,也忘了很多遍裸删,每隔一段時間就會感到模糊的一部分內(nèi)容兼呵。直到我閱讀了 Jakob Jenkov 大神這篇對初學者非常友好的 Java Memory Model砚作。我對其做了翻譯懂扼,一方面加深理解禁荸,便于日后復習右蒲,也希望能夠幫到更多需要的伙伴阀湿。【非逐字翻譯瑰妄,英文不錯的同學建議閱讀原文】
相比之前讀過的大部分書籍和博客陷嘴,這篇文章沒有在一開始就引入過多的細節(jié)。而是先以一個宏觀的視角切入间坐,讓讀者先對 JAVA 內(nèi)存模型有一個清晰的上層認識灾挨。再結(jié)合硬件內(nèi)存架構(gòu)模型邑退,講述了 JAVA 內(nèi)存模型與硬件內(nèi)存架構(gòu)模型的關(guān)系與區(qū)別(初學者非常容易混淆 JAVA 內(nèi)存模型和硬件內(nèi)存模型)。這兩點恰恰是我在學習 JAVA 內(nèi)存模型的過程中劳澄,最大的痛點地技。

為什么學習 JAVA 內(nèi)存模型

寬泛的說學習 JAVA 內(nèi)存模型能讓我們對 JAVA 程序的運行有一個更清晰的認識。更具體的秒拔,通過 JAVA 內(nèi)存模型莫矗,我們可以了解到不同線程對于共享的變量,是如何讀寫的砂缩。以及如何在必要的時候作谚,以同步的方式(syncronize)訪問共享變量。這對我們理解 JAVA 多線程編程庵芭,以及寫出正確的多線程并行程序十分重要妹懒。

JAVA 內(nèi)存模型

JAVA 內(nèi)存模型是 JVM 內(nèi)部的一種內(nèi)存模型,邏輯上可以主要分為線程棧(Thread Stack)和堆(Heap)兩部分双吆,如下圖所示:


Java Memory Model

線程棧(Thread Stack)

每個線程都擁有自己的線程棧眨唬,線程棧里面存放著相應(yīng)線程執(zhí)行方法(Method)時涉及的所有本地變量(local variables)。每個線程只能訪問自己的線程棧好乐,線程棧之間是互相不可見的单绑。
所有基本類型(boolean, byte, short, char, int, long, float, double)的本地變量是直接存儲于線程棧內(nèi)的,線程間均不可見曹宴。一個線程可能會通過拷貝的方式搂橙,把自己線程棧內(nèi)的基礎(chǔ)類型變量提供給另一個線程。但一定無法直接提供該變量本身笛坦。
所有對象類型的變量区转,棧中存儲的都只是一個引用,對象本身存儲于堆中版扩。

堆(heap)

JAVA 應(yīng)用中废离,所有的對象都是存儲在堆中的——包括對象版本的基礎(chǔ)類型(Byte, Integer, Long 等等)〗嘎可以總結(jié)為下圖:


Java Memory Model 2
  1. 基礎(chǔ)類型的本地變量是直接存儲在線程棧中的蜻韭。
  2. 非基礎(chǔ)類型的本地變量(即對象引用變量),線程棧中存儲的只是一個引用柿扣,實際的對象是存儲在堆中的肖方。
  3. 堆中對象可能會包含成員變量,這些成員變量無論是基礎(chǔ)類型變量未状,還是對象引用類型的變量俯画,都會隨對象存儲在堆中。
  4. 靜態(tài)變量司草,隨其所屬類一并存儲于堆中艰垂。

舉個例子

為了展示變量在線程棧和堆中的存儲情況泡仗,我們參照圖片 Java Memory Model 2,寫了如下代碼:

public class Main{
  public static void main(String[] args){
    Thread thread1 = new Thread(new MyRunnable());
    Thread thread2 = new Thread(new MyRunnable());
    thread1.start();
    thread2.start();
  }
}
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 member2 = 67890;
}

代碼中猜憎,兩個線程都會執(zhí)行 MyRunnable 類的 run 方法娩怎,run 方法調(diào)用 methodOne,methodOne 調(diào)用 methodTwo胰柑。最終各變量的存儲和關(guān)系可以描述為下圖:


Java Memory Model 3

結(jié)合代碼和這張圖峦树,我們應(yīng)該能清晰了解到 JAVA 代碼中各變量,實際運行時 JAVA 內(nèi)存模型中的存儲位置了旦事。

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

開頭我們說過魁巩,JAVA 內(nèi)存模型只是 JVM 內(nèi)部的一種內(nèi)存模型。它和我們熟悉的硬件內(nèi)存架構(gòu)模型有什么關(guān)系姐浮?又是如何一起工作的呢谷遂?
我們先了解一下硬件內(nèi)存架構(gòu),如下圖所示:


Hardware Memory Architecture 1

現(xiàn)在常見的電腦都是多 CPU 或者多核的卖鲤,這也是為什么我們的電腦可以實際支撐真實的多線程并行工作肾扰。在這樣的電腦上執(zhí)行多線程并行的 JAVA 程序時,不同的線程是有可能運行在不同的 CPU 上的蛋逾。
每個 CPU 都有一組寄存器(CPU Registers)—— CPU 內(nèi)部的內(nèi)存集晚。由于寄存器比主存(Main Memory)更快,CPU在操作存儲于寄存器的數(shù)據(jù)時区匣,會比操作主存數(shù)據(jù)快的多偷拔。
現(xiàn)在的 CPU 都還通常會有一個 CPU 緩存層(CPU Cache Memory Layer)。操作緩存層的速度介于寄存器和主存之間亏钩。(注:有的 CPU 也會設(shè)計多級緩存莲绰,比如 Cache Memory Layer1,Cache Memory Layer2 等姑丑,了解即可蛤签,不影響我們此處對 CPU 緩存的理解)
計算機都會有一個主存(Main Memory)。所有 CPU 都可以訪問它栅哀。
通常來說震肮,CPU 把需要的部分數(shù)據(jù)從主存拷貝到緩存,緩存中的部分數(shù)據(jù)會被拷貝到寄存器留拾,然后基于寄存器內(nèi)的數(shù)據(jù)完成計算戳晌,最終將結(jié)果逐級會寫到主存中。(在某個恰當?shù)臅r機將寄存器的數(shù)據(jù)寫回緩存间驮,然后再在某個恰當?shù)臅r機把緩存的數(shù)據(jù)寫回主存躬厌,比如我們需要釋放一部分緩存在存儲我們此時需要用到的其他數(shù)據(jù))。

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

硬件內(nèi)存架構(gòu)并不按照堆竞帽,棧區(qū)分扛施。實際上,JAVA 內(nèi)存模型中堆和棧存儲的數(shù)據(jù)屹篓,都會存儲到硬件內(nèi)存的主存上疙渣。而在某些時間點,部分的堆/棧數(shù)據(jù)也會出現(xiàn)在 CPU 緩存堆巧,或者寄存器上妄荔。如下圖所示:


Java Memory Model & Hardware Memory Architecture

一臺電腦有多個CPU,多個寄存器谍肤,多個緩存啦租。而我們的 JAVA 對象/變量可能存儲在這么多不同的位置,這就直接帶來了兩個問題:

  1. 共享變量(shared variables)在線程間的可見性問題
  2. 共享變量在多線程讀寫時的競爭條件(race condition)問題

共享變量的可見性問題

寫 JAVA 代碼時我們知道荒揣,在沒有正確使用 volatile 關(guān)鍵字或者 synchronization 時篷角,一個共享變量被線程A的修改,對線程B而言可能是不可見的系任。
這個比較好理解恳蹲,兩個運行于不同CPU的線程,分別從主存拷貝同一個變量到各自CPU的緩存甚至是寄存器中俩滥,由于他們后續(xù)一段時間對該變量的讀寫都僅僅發(fā)生在各自的緩存或寄存器內(nèi)的拷貝上嘉蕾,這些修改對不同線程間是不可見的。如下圖所示:

Visibility of Shared Objects 1

通過使用 volatile 關(guān)鍵字可以解決該問題霜旧。經(jīng)過 volatile 修飾的變量错忱,每次都會直接從主存讀取,并且保證每一次的修改都會回寫到主存上挂据。

競爭條件(race condition)

當多個線程想要同時修改同一個共享變量的時候航背,就會產(chǎn)生競爭條件問題。
假設(shè)我們有兩個執(zhí)行在不同CPU的線程:線程A和線程B棱貌。他們都讀取了主存中的一個共享變量 count = 1玖媚。然后分別在各自 CPU 緩存內(nèi)對其做了 +1 操作。原本我們期望的計算結(jié)果是 count + 1 + 1 = 3婚脱。但由于這兩次 +1 操作在不同的 CPU 緩存內(nèi)同時進行今魔,最終線程A和B將自己計算的結(jié)果回寫到主存時,結(jié)果為:count + 1 = 2障贸。如下圖所示:


Race Condition.png

該問題可以通過同步化來處理——保證一段代碼错森,同一時間,只能有一個線程執(zhí)行篮洁。JAVA 中同步化操作通過 synchronized 關(guān)鍵字實現(xiàn)涩维。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子瓦阐,更是在濱河造成了極大的恐慌蜗侈,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,002評論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件睡蟋,死亡現(xiàn)場離奇詭異踏幻,居然都是意外死亡,警方通過查閱死者的電腦和手機戳杀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,357評論 3 400
  • 文/潘曉璐 我一進店門该面,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人信卡,你說我怎么就攤上這事隔缀。” “怎么了傍菇?”我有些...
    開封第一講書人閱讀 169,787評論 0 365
  • 文/不壞的土叔 我叫張陵猾瘸,是天一觀的道長。 經(jīng)常有香客問我桥嗤,道長须妻,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,237評論 1 300
  • 正文 為了忘掉前任泛领,我火速辦了婚禮荒吏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘渊鞋。我一直安慰自己绰更,他們只是感情好,可當我...
    茶點故事閱讀 69,237評論 6 398
  • 文/花漫 我一把揭開白布锡宋。 她就那樣靜靜地躺著儡湾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪执俩。 梳的紋絲不亂的頭發(fā)上徐钠,一...
    開封第一講書人閱讀 52,821評論 1 314
  • 那天,我揣著相機與錄音役首,去河邊找鬼尝丐。 笑死,一個胖子當著我的面吹牛衡奥,可吹牛的內(nèi)容都是我干的爹袁。 我是一名探鬼主播,決...
    沈念sama閱讀 41,236評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼矮固,長吁一口氣:“原來是場噩夢啊……” “哼失息!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,196評論 0 277
  • 序言:老撾萬榮一對情侶失蹤盹兢,失蹤者是張志新(化名)和其女友劉穎邻梆,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蛤迎,經(jīng)...
    沈念sama閱讀 46,716評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡确虱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,794評論 3 343
  • 正文 我和宋清朗相戀三年含友,在試婚紗的時候發(fā)現(xiàn)自己被綠了替裆。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,928評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡窘问,死狀恐怖辆童,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情惠赫,我是刑警寧澤把鉴,帶...
    沈念sama閱讀 36,583評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站儿咱,受9級特大地震影響庭砍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜混埠,卻給世界環(huán)境...
    茶點故事閱讀 42,264評論 3 336
  • 文/蒙蒙 一怠缸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧钳宪,春花似錦揭北、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,755評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至半醉,卻和暖如春疚俱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背缩多。 一陣腳步聲響...
    開封第一講書人閱讀 33,869評論 1 274
  • 我被黑心中介騙來泰國打工呆奕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瞧壮。 一個月前我還...
    沈念sama閱讀 49,378評論 3 379
  • 正文 我出身青樓登馒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親咆槽。 傳聞我的和親對象是個殘疾皇子陈轿,可洞房花燭夜當晚...
    茶點故事閱讀 45,937評論 2 361

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