1. 程式人生 > >Java記憶體模型深度解讀

Java記憶體模型深度解讀

Java記憶體模型規範了Java虛擬機器與計算機記憶體是如何協同工作的。Java虛擬機器是一個完整的計算機的一個模型,因此這個模型自然也包含一個記憶體模型——又稱為Java記憶體模型。

如果你想設計表現良好的併發程式,理解Java記憶體模型是非常重要的。Java記憶體模型規定了如何和何時可以看到由其他執行緒修改過後的共享變數的值,以及在必須時如何同步的訪問共享變數。

原始的Java記憶體模型存在一些不足,因此Java記憶體模型在Java1.5時被重新修訂。這個版本的Java記憶體模型在Java8中人在使用。

Java記憶體模型內部原理

Java記憶體模型把Java虛擬機器內部劃分為執行緒棧和堆。這張圖演示了Java記憶體模型的邏輯檢視。

每一個執行在Java虛擬機器裡的執行緒都擁有自己的執行緒棧。這個執行緒棧包含了這個執行緒呼叫的方法當前執行點相關的資訊。一個執行緒僅能訪問自己的執行緒棧。一個執行緒建立的本地變數對其它執行緒不可見,僅自己可見。即使兩個執行緒執行同樣的程式碼,這兩個執行緒任然在在自己的執行緒棧中的程式碼來建立本地變數。因此,每個執行緒擁有每個本地變數的獨有版本。

所有原始型別的本地變數都存放線上程棧上,因此對其它執行緒不可見。一個執行緒可能向另一個執行緒傳遞一個原始型別變數的拷貝,但是它不能共享這個原始型別變數自身。

堆上包含在Java程式中建立的所有物件,無論是哪一個物件建立的。這包括原始型別的物件版本。如果一個物件被建立然後賦值給一個區域性變數,或者用來作為另一個物件的成員變數,這個物件任然是存放在堆上。

下面這張圖演示了呼叫棧和本地變數存放線上程棧上,物件存放在堆上。

一個本地變數可能是原始型別,在這種情況下,它總是“呆在”執行緒棧上。

一個本地變數也可能是指向一個物件的一個引用。在這種情況下,引用(這個本地變數)存放線上程棧上,但是物件本身存放在堆上。

一個物件可能包含方法,這些方法可能包含本地變數。這些本地變數任然存放線上程棧上,即使這些方法所屬的物件存放在堆上。

一個物件的成員變數可能隨著這個物件自身存放在堆上。不管這個成員變數是原始型別還是引用型別。

靜態成員變數跟隨著類定義一起也存放在堆上。

存放在堆上的物件可以被所有持有對這個物件引用的執行緒訪問。當一個執行緒可以訪問一個物件時,它也可以訪問這個物件的成員變數。如果兩個執行緒同時呼叫同一個物件上的同一個方法,它們將會都訪問這個物件的成員變數,但是每一個執行緒都擁有這個本地變數的私有拷貝。

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

兩個執行緒擁有一些列的本地變數。其中一個本地變數(Local Variable 2)執行堆上的一個共享物件(Object 3)。這兩個執行緒分別擁有同一個物件的不同引用。這些引用都是本地變數,因此存放在各自執行緒的執行緒棧上。這兩個不同的引用指向堆上同一個物件。

注意,這個共享物件(Object 3)持有Object2和Object4一個引用作為其成員變數(如圖中Object3指向Object2和Object4的箭頭)。通過在Object3中這些成員變數引用,這兩個執行緒就可以訪問Object2和Object4。

這張圖也展示了指向堆上兩個不同物件的一個本地變數。在這種情況下,指向兩個不同物件的引用不是同一個物件。理論上,兩個執行緒都可以訪問Object1和Object5,如果兩個執行緒都擁有兩個物件的引用。但是在上圖中,每一個執行緒僅有一個引用指向兩個物件其中之一。

因此,什麼型別的Java程式碼會導致上面的記憶體圖呢?如下所示:

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;
}

如果兩個執行緒同時執行run()方法,就會出現上圖所示的情景。run()方法呼叫methodOne()方法,methodOne()呼叫methodTwo()方法。methodOne()聲明瞭一個原始型別的本地變數和一個引用型別的本地變數。

每個執行緒執行methodOne()都會在它們對應的執行緒棧上建立localVariable1localVariable2的私有拷貝。localVariable1變數彼此完全獨立,僅“生活”在每個執行緒的執行緒棧上。一個執行緒看不到另一個執行緒對它的localVariable1私有拷貝做出的修改。

每個執行緒執行methodOne()時也將會建立它們各自的localVariable2拷貝。然而,兩個localVariable2的不同拷貝都指向堆上的同一個物件。程式碼中通過一個靜態變數設定localVariable2指向一個物件引用。僅存在一個靜態變數的一份拷貝,這份拷貝存放在堆上。因此,localVariable2的兩份拷貝都指向由MySharedObject指向的靜態變數的同一個例項。MySharedObject例項也存放在堆上。它對應於上圖中的Object3。

注意,MySharedObject類也包含兩個成員變數。這些成員變數隨著這個物件存放在堆上。這兩個成員變數指向另外兩個Integer物件。這些Integer物件對應於上圖中的Object2和Object4.

注意,methodTwo()建立一個名為localVariable的本地變數。這個成員變數是一個指向一個Integer物件的物件引用。這個方法設定localVariable1引用指向一個新的Integer例項。在執行methodTwo方法時,localVariable1引用將會在每個執行緒中存放一份拷貝。這兩個Integer物件例項化將會被儲存堆上,但是每次執行這個方法時,這個方法都會建立一個新的Integer物件,兩個執行緒執行這個方法將會建立兩個不同的Integer例項。methodTwo方法建立的Integer物件對應於上圖中的Object1和Object5。

還有一點,MySharedObject類中的兩個long型別的成員變數是原始型別的。因為,這些變數是成員變數,所以它們任然隨著該物件存放在堆上,僅有本地變數存放線上程棧上。

硬體記憶體架構

現代硬體記憶體模型與Java記憶體模型有一些不同。理解記憶體模型架構以及Java記憶體模型如何與它協同工作也是非常重要的。這部分描述了通用的硬體記憶體架構,下面的部分將會描述Java記憶體是如何與它“聯手”工作的。

下面是現代計算機硬體架構的簡單圖示:

一個現代計算機通常由兩個或者多個CPU。其中一些CPU還有多核。從這一點可以看出,在一個有兩個或者多個CPU的現代計算機上同時執行多個執行緒是可能的。每個CPU在某一時刻執行一個執行緒是沒有問題的。這意味著,如果你的Java程式是多執行緒的,在你的Java程式中每個CPU上一個執行緒可能同時(併發)執行。

每個CPU都包含一系列的暫存器,它們是CPU內記憶體的基礎。CPU在暫存器上執行操作的速度遠大於在主存上執行的速度。這是因為CPU訪問暫存器的速度遠大於主存。

每個CPU可能還有一個CPU快取層。實際上,絕大多數的現代CPU都有一定大小的快取層。CPU訪問快取層的速度快於訪問主存的速度,但通常比訪問內部暫存器的速度還要慢一點。一些CPU還有多層快取,但這些對理解Java記憶體模型如何和記憶體互動不是那麼重要。只要知道CPU中可以有一個快取層就可以了。

一個計算機還包含一個主存。所有的CPU都可以訪問主存。主存通常比CPU中的快取大得多。

通常情況下,當一個CPU需要讀取主存時,它會將主存的部分讀到CPU快取中。它甚至可能將快取中的部分內容讀到它的內部暫存器中,然後在暫存器中執行操作。當CPU需要將結果寫回到主存中去時,它會將內部暫存器的值重新整理到快取中,然後在某個時間點將值重新整理回主存。

當CPU需要在快取層存放一些東西的時候,存放在快取中的內容通常會被重新整理回主存。CPU快取可以在某一時刻將資料區域性寫到它的記憶體中,和在某一時刻區域性重新整理它的記憶體。它不會再某一時刻讀/寫整個快取。通常,在一個被稱作“cache lines”的更小的記憶體塊中快取被更新。一個或者多個快取行可能被讀到快取,一個或者多個快取行可能再被重新整理回主存。

Java記憶體模型和硬體記憶體架構之間的橋接

上面已經提到,Java記憶體模型與硬體記憶體架構之間存在差異。硬體記憶體架構沒有區分執行緒棧和堆。對於硬體,所有的執行緒棧和堆都分佈在主內中。部分執行緒棧和堆可能有時候會出現在CPU快取中和CPU內部的暫存器中。如下圖所示:

當物件和變數被存放在計算機中各種不同的記憶體區域中時,就可能會出現一些具體的問題。主要包括如下兩個方面:

  • 執行緒對共享變數修改的可見性
  • 當讀,寫和檢查共享變數時出現race conditions

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

共享物件可見性

如果兩個或者更多的執行緒在沒有正確的使用宣告或者同步的情況下共享一個物件,一個執行緒更新這個共享物件可能對其它執行緒來說是不接見的。

想象一下,共享物件被初始化在主存中。跑在CPU上的一個執行緒將這個共享物件讀到CPU快取中。然後修改了這個物件。只要CPU快取沒有被重新整理會主存,物件修改後的版本對跑在其它CPU上的執行緒都是不可見的。這種方式可能導致每個執行緒擁有這個共享物件的私有拷貝,每個拷貝停留在不同的CPU快取中。

下圖示意了這種情形。跑在左邊CPU的執行緒拷貝這個共享物件到它的CPU快取中,然後將count變數的值修改為2。這個修改對跑在右邊CPU上的其它執行緒是不可見的,因為修改後的count的值還沒有被重新整理回主存中去。

解決這個問題你可以使用Java中的volatile關鍵字。volatile關鍵字可以保證直接從主存中讀取一個變數,如果這個變數被修改後,總是會被寫回到主存中去。

Race Conditions

如果兩個或者更多的執行緒共享一個物件,多個執行緒在這個共享物件上更新變數,就有可能發生race conditions

想象一下,如果執行緒A讀一個共享物件的變數count到它的CPU快取中。再想象一下,執行緒B也做了同樣的事情,但是往一個不同的CPU快取中。現線上程A將count加1,執行緒B也做了同樣的事情。現在count已經被增在了兩個,每個CPU快取中一次。

如果這些增加操作被順序的執行,變數count應該被增加兩次,然後原值+2被寫回到主存中去。

然而,兩次增加都是在沒有適當的同步下併發執行的。無論是執行緒A還是執行緒B將count修改後的版本寫回到主存中取,修改後的值僅會被原值大1,儘管增加了兩次。

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

解決這個問題可以使用Java同步塊。一個同步塊可以保證在同一時刻僅有一個執行緒可以進入程式碼的臨界區。同步塊還可以保證程式碼塊中所有被訪問的變數將會從主存中讀入,當執行緒退出同步程式碼塊時,所有被更新的變數都會被重新整理回主存中去,不管這個變數是否被宣告為volatile

相關推薦

Java記憶體模型深度解讀

Java記憶體模型規範了Java虛擬機器與計算機記憶體是如何協同工作的。Java虛擬機器是一個完整的計算機的一個模型,因此這個模型自然也包含一個記憶體模型——又稱為Java記憶體模型。 如果你想設計表現良好的併發程式,理解Java記憶體模型是非常重要的。Java記憶體模型規定了如何和何時可以看到由其他執行緒

Java 記憶體模型基礎

一、併發程式設計模型的兩個關鍵問題 1. 執行緒之間如何通訊 通訊是指執行緒之間以何種機制來交換資訊。 在指令式程式設計中,執行緒之間的通訊機制有兩種:共享記憶體和訊息傳遞。 在共享記憶體的併發模型裡,執行緒之間共享程式的公共狀態,通過寫-讀記憶體中的公共狀

十一、JVM(HotSpot)Java記憶體模型與執行緒

注:本博文主要是基於JDK1.7會適當加入1.8內容。 1、Java記憶體模型 記憶體模型:在特定的操作協議下,對特定的記憶體或快取記憶體進行讀寫訪問的抽象過程。不同的物理機擁有不一樣的記憶體模型,而Java虛擬機器也擁有自己的記憶體模型。 主要目標:定義程式中各個變數的訪問規則,

Java併發(四):volatile的實現原理 Java併發(一):Java記憶體模型乾貨總結

synchronized是一個重量級的鎖,volatile通常被比喻成輕量級的synchronized volatile是一個變數修飾符,只能用來修飾變數。 volatile寫:當寫一個volatile變數時,JMM會把該執行緒對應的本地記憶體中的共享變數重新整理到主記憶體。 volatile讀:當讀一

Java記憶體模型解析

一.java記憶體模型的誕生原因以及作用   1.誕生原因     java虛擬機器中規範定義的一種記憶體模型,來遮蔽調各種硬體和作業系統之間的記憶體訪問差異,為了實現java程式在各種平臺都能達到一致的記憶體訪問效果。這是它誕生的緣由。   2.作用     其實這個記憶體模型主要就是去定義程式中各

Java記憶體模型(2)——happens-before

happens-before 原則 (先行發生原則)是JMM中最核心的概念,該原則闡述了操作之間的記憶體可見性。 happens-before的誕生——完善的JMM  Java語言是最早嘗試提供記憶體模型的語言,這是簡化多執行緒程式設計、保證程式可移植性的一個飛躍

Java記憶體模型(1)——JMM

在學習Java併發程式設計中,瞭解Java記憶體模型對於我們去理解Java多執行緒程式設計是非常有幫助的,本文將對JMM進行一個大體介紹,讓我們對JMM有一個大體的輪廓。 硬體的記憶體模型  為了提高效率,充分利用計算機的能力,多工處理已經成為現代計算機的必備功能

JVM學習之java記憶體模型

JVM學習之java記憶體模型 以下blog內容來自《深入理解Java虛擬機器_JVM高階特性與最佳實踐》感謝作者!! java虛擬機器規範定義了一種java記憶體模型(JMM)來遮蔽不同硬體和作業系統的差異,達到跨平臺執行效果,記憶體模型的定義一個宗旨就是併發記憶體訪問操作不會產生

[Java記憶體模型]happens-before

重排序需要遵守happens-before規則,如果不符合,則編譯器和處理器不會對程式進行優化重排序 從JDK5開始,java使用新的JSR -133記憶體模型(本文除非特別說明,針對的都是JSR- 133記憶體模型)。JSR-133使用happens-before的概念來

JVM記憶體結構、Java記憶體模型以及Java物件模型之間的區別

Java作為一種面向物件的,跨平臺語言,其物件、記憶體等一直是比較難的知識點。而且很多概念的名稱看起來又那麼相似,很多人會傻傻分不清楚。比如本文我們要討論的JVM記憶體結構、Java記憶體模型和Java物件模型,這就是三個截然不同的概念,但是很多人容易弄混。 可以這樣說,很多高階開發甚至都搞

三、Java記憶體模型---重排序和順序一致性

3.2 重排序 重排序是指編譯器和處理器為了優化程式效能而對指令序列進行重新排序的一種手段。 3.2.1 資料依賴性 如果兩個操作訪問同一個變數,且這兩個操作中有一個為寫操作,此時這兩個操作之間 就存在資料依賴性。資料依賴分為下列3種類型,如表3-4所示。 上面3種情況,只

三、Java記憶體模型---Java記憶體模型的基礎

3.1 Java記憶體模型的基礎 3.1.1 併發程式設計模型的兩個關鍵問題 併發程式設計中,有兩大關鍵問題:執行緒之間如何通訊和執行緒之間如何同步。通訊是指執行緒之間以何種機制來交換資訊。在指令式程式設計中,執行緒之間的通訊機制有兩種:共享記憶體和訊息傳遞。 在共享記憶體的併發模

JAVA記憶體模型入門

概述 jvm 入門篇,想要學習jvm,必須先得了解JVM記憶體模型,JVM記憶體模型,JVM記憶體模型,JVM記憶體模型,JVM記憶體模型。重要的事情說多遍。 記憶體劃分 java虛擬機器按照執行時記憶體使用區域劃分如圖:   Paste_Image.png

Java記憶體模型解析volatile關鍵字

   面試裡面多執行緒是繞不開的話題,補習多執行緒的時候看到一篇好文章,從Java記憶體模型的角度解析volatile關鍵字,對於多執行緒新手的我來說收穫很多。      原文連結:   http://www.cnblogs.com/dolphin0520/p/3920373.html

由淺入深Java記憶體模型

轉發來源 JMM Java記憶體模型描述了Java程式中各種變數(共享變數)的訪問規則,以及在JVM中將變數儲存到記憶體和從記憶體中讀取這些變數的底層細節。 主存:所有共享變數都儲存在主存中。 工作記憶體:每個執行緒都有自己

知識點整理2:Java記憶體模型

原子性、記憶體可見性、重排序、順序一致性、volatile、鎖、final 一、原子性 原子性操作指相應的操作是單一不可分割的操作。例如,對int變數count執行count++d操作就不是原子性操作。因為count++實際上可以分解為3個操作:(1)讀取變數count的當前值

Java記憶體模型乾貨總結

併發程式設計模型  關鍵問題:執行緒之間如何通訊 執行緒之間如何同步 共享記憶體模型(例:java):執行緒之間共享程式的公共狀態,執行緒之間通過寫-讀記憶體中的公共狀態來隱式進行通訊   同步是顯式進行的。程式設計師必須顯式指定某個方法或某段程式碼需要線上程之間互斥執行 訊息傳遞模型:執行

《深入理解 Java 記憶體模型》讀書筆記(下)(乾貨,萬字長文)

0. 前提 1. 基礎 2. 重排序 3. 順序一致性 4. Volatile 5. 鎖 6. final 7. 總結 4. Volatile 4.1 VOLATILE 特性 舉個例子: publ

《深入理解 Java 記憶體模型》讀書筆記(上)(乾貨,萬字長文)

0. 前提 1. 基礎 2. 重排序 3. 順序一致性 4. Volatile 5. 鎖 6. final 7. 總結 0. 前提 《深入理解 Java 記憶體模型》 程曉明著,該書在以前看過一

java記憶體模型的happens-before語義順序問題?

java記憶體模型的happens-before語義順序問題? 注意,兩個操作之間具有 happens-before 關係,並不意味著前一個操作必須要在後一個操作之前執行!happens-before 僅僅要求前一個操作(執行的結果)對後 一個操作可見,且前一個操作按順序排在第二個操作之前(th