Java記憶體模型---並發編程網 - ifeve.com

來源:互聯網
上載者:User

標籤:

Java記憶體模型

 轉自:http://ifeve.com/java-memory-model-6/

原文地址  Jakob Jenkov 譯者:張坤

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

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

共用對象可見度

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

想象一下,共用對象被初始化在主存中。跑在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記憶體模型---並發編程網 - ifeve.com

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.