【譯】Java內(nèi)存模型

翻譯鏈接:http://tutorials.jenkov.com/java-concurrency/java-memory-model.html


內(nèi)容:

? Java內(nèi)存模型

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

? 使以上兩者相互聯(lián)系的

? 共享對(duì)象的可見(jiàn)性

? 競(jìng)爭(zhēng)狀態(tài)


Java內(nèi)存模型指定了JVM如何與計(jì)算機(jī)內(nèi)存RAM協(xié)作。

若你想正確地設(shè)計(jì)并發(fā)行為,那必須得好好理解Java的內(nèi)存模型(以下簡(jiǎn)稱JMM)。JMM指定了各個(gè)線程在需要時(shí)如何獲得其他線程的變量值,又怎樣去同步訪問(wèn)這些共享的變量。

原始的JMM是并不滿足需求,因而在Java1.5中JMM被重新修訂,并仍在Java8中使用。


內(nèi)部JMM

JVM在為線程棧(thread stacks)和堆(heap)分配內(nèi)存時(shí)使用到了JMM。以下的圖表從表現(xiàn)了JMM的邏輯視角:


每一個(gè)在JVM中運(yùn)行的線程擁有自己的線程棧。線程棧中包含了當(dāng)前該線程擁有的、能夠被調(diào)用執(zhí)行的方法。我將稱之為“調(diào)用?!保╟all stack)。當(dāng)線程執(zhí)行這些代碼時(shí),該調(diào)用棧也會(huì)相應(yīng)地改變。

線程棧中包含了每個(gè)被執(zhí)行的方法的所有的本地變量(local variable)(所有的方法都在調(diào)用棧中)。每個(gè)線程只能訪問(wèn)它自己的線程棧。線程創(chuàng)建的本地變量對(duì)于其他線程來(lái)說(shuō)是不可見(jiàn)的。即使兩個(gè)線程在執(zhí)行相同的代碼,這兩個(gè)線程仍然會(huì)在各自的線程棧中創(chuàng)建這段代碼中的該本地變量。因此,每一個(gè)線程擁有自己版本的每個(gè)本地變量。

所有的原始數(shù)據(jù)類型(boolean, byte, short, char, int, long, float, double)的本地變量都被存儲(chǔ)在線程棧中,因而對(duì)于其他線程而言是不可見(jiàn)的。一個(gè)線程可能通過(guò)傳遞一份原始數(shù)據(jù)類型變量的拷貝給另一個(gè)線程,但不能共享它們給其他線程。

堆包含Java應(yīng)用無(wú)論哪個(gè)線程創(chuàng)建的對(duì)象。它還包括一些原始數(shù)據(jù)類型(e.g. Byte, Integer, Long etc.)。不管這些對(duì)象賦值給本地變量,還是在另一個(gè)對(duì)象中作為成員變量被創(chuàng)建,它們都將被存儲(chǔ)在堆內(nèi)存中。

下圖表示了調(diào)用棧和本地變量是存儲(chǔ)在線程棧中,而對(duì)象是在堆內(nèi)存中。

一個(gè)本地變量可能是原始數(shù)據(jù)類型,在這種情況下它們被存儲(chǔ)在線程棧中。

一個(gè)本地變量也可能指向一個(gè)對(duì)象,這時(shí)本地變量仍然存儲(chǔ)在線程棧中,該對(duì)象也仍在堆內(nèi)存中。

一個(gè)對(duì)象可能包含有本地變量的方法。方法中的變量還是在線程棧中,即使該對(duì)象是在堆內(nèi)存中。

一個(gè)對(duì)象的成員變量和該對(duì)象一起存儲(chǔ)在堆內(nèi)存中。即使成員變量是原始類型或指向一個(gè)對(duì)象。

靜態(tài)類變量和它的類定義一起存儲(chǔ)在堆中。

堆中的對(duì)象可以被所有線程訪問(wèn),當(dāng)這些線程中有變量指向它,線程還可以訪問(wèn)對(duì)象中的成員變量。若兩個(gè)線程同時(shí)調(diào)用同一個(gè)對(duì)象的同一方法,它們能夠訪問(wèn)該對(duì)象的成員變量,每個(gè)線程將復(fù)制各自版本的本地變量。

以下是圖解:

兩個(gè)線程都擁有各自的一組線程棧中的成員變量,其中Local Variable2指向了一個(gè)堆中的共享對(duì)象Object 3.兩個(gè)線程每個(gè)都擁有不同的本地變量指向Object3,兩個(gè)本地變量都在各自的線程棧中,雖然它們指向了堆中的同一個(gè)對(duì)象。

注意共享對(duì)象Object3 同時(shí)被成員變量Object2和Object4指向,因此兩個(gè)線程還能訪問(wèn)Object2和Object4。

這張圖表還展示了一個(gè)本地對(duì)象指向堆中的兩個(gè)不同的對(duì)象。在這種情況下了,按理論來(lái)說(shuō),兩個(gè)線程都能訪問(wèn)Object1和Object5,只要兩個(gè)線程都用引用到這兩個(gè)對(duì)象。但是在這張圖表中,每個(gè)一線程都只有一個(gè)引用指向其中一個(gè)對(duì)象。

那么,怎樣的java代碼可以展現(xiàn)上面的內(nèi)存邏輯圖?以下便是一個(gè)簡(jiǎn)單的例子。


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è)線程都執(zhí)行了run()方法,那么上面的圖表便是結(jié)果表示了。run()方法會(huì)調(diào)用methodOne(),接著methodOne會(huì)調(diào)用methosTwo()。methodOne()聲明了一個(gè)原始數(shù)據(jù)類型的本地變量(lovalVariable1是int類型)和一個(gè)指向了對(duì)象的的本地變量localVariable2。

每一個(gè)線程執(zhí)行methodOne()都會(huì)創(chuàng)建一份它們自己的localVariable1和localVariable2在各自的線程棧中。其中兩者的localVariable1是完全無(wú)關(guān)的,只存在于各自的線程棧中。一個(gè)線程不能看見(jiàn)另一個(gè)線程對(duì)它的本地變量做了什么操作。

每一個(gè)線程執(zhí)行methodOne方法還會(huì)創(chuàng)建一份localVariable2的復(fù)制,但是這兩份復(fù)制都最終指向了堆中的同一個(gè)對(duì)象。這塊代碼中的localVariable2都指向一個(gè)靜態(tài)變量對(duì)象。靜態(tài)變量在內(nèi)存中只有一份復(fù)制,該復(fù)制存儲(chǔ)于堆內(nèi)存中。因此,兩個(gè)localVariable2都指向同一個(gè)MySharedObject實(shí)例,并且該實(shí)例也是被存儲(chǔ)于堆內(nèi)存中。它和圖中的Object3相一致。

要注意MySharedObject也包含了兩個(gè)成員變量。成員變量和類對(duì)象一起被存儲(chǔ)在堆內(nèi)存中。這兩個(gè)成員變量都指向了兩個(gè)Integer對(duì)象。這兩個(gè)Integer對(duì)象和圖中的Object2和Object4相一致。

注意methodTwo方法創(chuàng)建了一個(gè)名為localVariable1的本地變量。這個(gè)成員變量是對(duì)Integer對(duì)象的對(duì)象引用。這個(gè)方法使得localVariable1的引用指向一個(gè)新的Integer實(shí)例。localVariable1的引用會(huì)在每個(gè)執(zhí)行methodTwo方法的線程中復(fù)制并存儲(chǔ)。這兩個(gè)Integer對(duì)象被實(shí)例化后會(huì)被存儲(chǔ)在堆內(nèi)存中。但是每當(dāng)這個(gè)方法被執(zhí)行時(shí),一個(gè)新的Integer對(duì)象就將會(huì)被創(chuàng)建。在方法methodTwo中創(chuàng)建的Integer對(duì)象對(duì)應(yīng)上圖中的Object1和Object5。

注意MySharedObject類中的long類型成員變量是原始數(shù)據(jù)類型,因?yàn)樗鼈兪浅蓡T變量,因此仍然會(huì)被存儲(chǔ)在堆內(nèi)存中,只有本地變量才會(huì)存儲(chǔ)在線程棧中。


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

為了理解JMM是如何與硬件內(nèi)存架構(gòu)合作,理解它也變的非常重要,因?yàn)楝F(xiàn)代硬件內(nèi)存架構(gòu)和java內(nèi)部?jī)?nèi)存模型是不太一樣的。

以下是簡(jiǎn)單化的現(xiàn)代計(jì)算機(jī)硬件架構(gòu)。

現(xiàn)代計(jì)算機(jī)常常有兩個(gè)或以上的CPU,一些CPU還是多核的。關(guān)鍵是,這樣的多CPU硬件特性使得多個(gè)線程同時(shí)運(yùn)行成為可能。每一個(gè)CPU都可以在任何時(shí)刻運(yùn)行一個(gè)線程。這意味著如果你的Java應(yīng)用是多線程的,每一個(gè)CPU都有一個(gè)線程同時(shí)在運(yùn)行。

每一個(gè)CPU都包含一組寄存器,它們是CPU內(nèi)存的基礎(chǔ)。CPU在寄存器上進(jìn)行操作變量的速度遠(yuǎn)遠(yuǎn)快于在主存上,這是因?yàn)镃PU訪問(wèn)寄存器的速度遠(yuǎn)快于訪問(wèn)主存的速度。

每一個(gè)CPU可能還會(huì)有一個(gè)CPU緩存層。事實(shí)上,大部分現(xiàn)代的CPU都會(huì)有一定大小的CPU緩存。CPU訪問(wèn)緩存層的速度大于訪問(wèn)主存,一般而言小于寄存器。一些CPU可能會(huì)有還幾個(gè)級(jí)別的緩存(Level 1,Level2),JMM如何與之交互在這里并不是重點(diǎn),關(guān)鍵是要知道CPU有一個(gè)緩存層。

計(jì)算機(jī)包含一個(gè)主存區(qū)(RAM),所有的CPU都可以訪問(wèn)RAM,其大小往往遠(yuǎn)大于CPU的緩存區(qū)。

通常,當(dāng)CPU需要訪問(wèn)主存,它會(huì)讀取一部分的主存內(nèi)容到CPU緩存,甚至?xí)x取一些緩存到寄存器里,然后再進(jìn)行操作。當(dāng)CPU需要把結(jié)果送回到主存時(shí),它會(huì)把結(jié)果流入到緩存,再?gòu)木彺媪魅氲街鞔妗.?dāng)CPU需要在緩存數(shù)據(jù)的時(shí)候,之前在緩存中的數(shù)據(jù)往往會(huì)流回到主存中。緩存在一段時(shí)間內(nèi)流入數(shù)據(jù),在另一段時(shí)間內(nèi)流出數(shù)據(jù)。每一次更新的時(shí)候它不需要把整個(gè)緩存讀或?qū)?。通常,緩存更新的只是被稱為“緩存線”的小內(nèi)存塊,只是一個(gè)或多個(gè)的內(nèi)存線一遍遍被寫(xiě)或被讀。


使兩者相聯(lián)系的橋梁

根據(jù)前面已經(jīng)說(shuō)明的,JMM和硬件內(nèi)存架構(gòu)是不一樣的。硬件內(nèi)存架構(gòu)并不區(qū)分線程棧和堆。在硬件中,線程棧和堆都在主存中。部分線程棧和堆可能會(huì)在CPU緩存中,或者CPU內(nèi)部的寄存器中。如下圖所示。


當(dāng)對(duì)象和變量存儲(chǔ)在計(jì)算機(jī)的不同存儲(chǔ)部位的時(shí)候,可能會(huì)產(chǎn)生以下兩個(gè)主要問(wèn)題:

1.線程更新共享變量時(shí),變量的可見(jiàn)性。

2.當(dāng)讀寫(xiě)共享變量時(shí)的競(jìng)爭(zhēng)機(jī)制。

以上問(wèn)題將會(huì)在下幾節(jié)中解釋。

共享對(duì)象的可見(jiàn)性

如果一個(gè)以上的線程共享同一個(gè)對(duì)象,而該對(duì)象又沒(méi)有適宜地使用volatile聲明,或者沒(méi)有使用同步方法,那么對(duì)于其他線程來(lái)說(shuō)這個(gè)更新可能是不可見(jiàn)的。

想象一下剛開(kāi)始共享對(duì)象實(shí)在主存中被初始化。CPU1上的一個(gè)線程讀取這個(gè)共享對(duì)象到CPU1緩存中,在那里改變了該對(duì)象值。只要CPU緩存沒(méi)有把數(shù)據(jù)流回到主存中,被改變的共享對(duì)象對(duì)于其他線程便是不可見(jiàn)的。這種情況下,每一個(gè)線程都有一份共享對(duì)象的最終值位于它們所在CPU的緩存中。

下圖便展現(xiàn)了上述情況。在左邊CPU的一個(gè)線程復(fù)制了一份共享對(duì)象于它的緩存中,并且把它的count變量改變成了2。這個(gè)改動(dòng)對(duì)于其他在右邊CPU上的線程來(lái)說(shuō)是不可見(jiàn)的,因?yàn)閏ount的更新后的數(shù)值并沒(méi)有被流回主存。




為了解決這種情況,你可以使用

volatile

關(guān)鍵字,它可以確保給定的變量可以直接從主存中讀取,一旦被更新又能馬上回到主存。

競(jìng)爭(zhēng)機(jī)制

如果兩個(gè)或以上的線程共享一個(gè)對(duì)象,那么會(huì)有多個(gè)線程更新的共享對(duì)象,競(jìng)爭(zhēng)將會(huì)發(fā)生。

如果線程A讀取共享對(duì)象的變量count到它的緩存中,同時(shí)線程B也做了相同的事情,但是在另一個(gè)CPU緩存中。A對(duì)count進(jìn)行了一次加1的操作,B也做了如此,那么現(xiàn)在var1已經(jīng)被增加了兩次,在兩個(gè)不同的CPU緩存中。

如果這些操作是依次進(jìn)行的,那么count變量是被增加兩次,并且以原值加2 后的數(shù)值返回主存。

但是如果這兩次操作是并發(fā)且沒(méi)有合適的同步,那么盡管兩個(gè)線程都對(duì)count進(jìn)行了增加并返回了主存,那么主存里count的更新后數(shù)值還是比原值大1,盡管它被增加過(guò)兩次。

下圖就是說(shuō)明了上述的主要意思。


為了解決這個(gè)問(wèn)題,你可以使用Java的synchronized塊。同步塊確保任何時(shí)間只能有一個(gè)線程能夠訪問(wèn)給定的代碼塊。同步塊還能保證代碼塊中的所有變量只能從主存中讀取,當(dāng)線程退出同步塊的時(shí)候,所有已經(jīng)更新的變量會(huì)流回到主存,無(wú)論它們是否被聲明volatile。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容