Java 并發(fā)/多線程教程(十一)-JAVA內(nèi)存模型

本系列譯自jakob jenkov的Java并發(fā)多線程教程,個(gè)人覺得很有收獲。由于個(gè)人水平有限,不對(duì)之處還望矯正!

? ? ? ? Java內(nèi)存模型指定Java虛擬機(jī)如何與計(jì)算機(jī)的內(nèi)存(RAM)一起工作。Java虛擬機(jī)是整個(gè)計(jì)算機(jī)的一個(gè)模型,所以這個(gè)模型自然包含了一個(gè)內(nèi)存模型——也就是Java內(nèi)存模型。

? ? ? ? 如果您想要設(shè)計(jì)正確的并發(fā)程序,那么理解Java內(nèi)存模型是非常重要的。Java內(nèi)存模型指定了不同線程如何以及何時(shí)可以看到由其他線程寫入共享變量的值,以及在必要時(shí)如何同步訪問共享變量。

? ? ? ? 原來的Java內(nèi)存模型不夠用,所以Java內(nèi)存模型在Java 1.5中被修改了。Java內(nèi)存模型的這個(gè)版本仍然在Java 8中使用。

內(nèi)部JAVA內(nèi)存模型

? ? ? JVM內(nèi)部使用的Java內(nèi)存模型劃分了線程棧和堆之間的內(nèi)存。這個(gè)圖表從邏輯的角度演示了Java內(nèi)存模型

? ? ? ? 在Java虛擬機(jī)中運(yùn)行的每個(gè)線程都有自己的線程棧。線程棧包含關(guān)于線程調(diào)用什么方法來達(dá)到當(dāng)前執(zhí)行點(diǎn)的信息。我將把它稱為“調(diào)用?!?。當(dāng)線程執(zhí)行其代碼時(shí),調(diào)用堆棧會(huì)發(fā)生變化。

? ? ? ? 線程棧還包含每個(gè)正在執(zhí)行的方法的所有本地變量(調(diào)用棧上的所有方法)。線程只能訪問它自己的線程棧。線程創(chuàng)建的局部變量對(duì)于所有其他線程都是不可見的,而不是創(chuàng)建線程的線程。即使兩個(gè)線程在執(zhí)行完全相同的代碼,兩個(gè)線程仍然會(huì)在各自的線程棧中創(chuàng)建該代碼的本地變量。因此,每個(gè)線程都有自己的每個(gè)局部變量的版本。所有基本數(shù)據(jù)類型本地變量如(boolean、byte、short、char、int、long、float、double)都被完全存儲(chǔ)在線程棧中,因此對(duì)其他線程來說是不可見的。一個(gè)線程可能將pritimive變量的副本傳遞給另一個(gè)線程,但是它不能共享原始的局部變量本身。

? ? ? ? 堆包含在Java應(yīng)用程序中創(chuàng)建的所有對(duì)象,而不管創(chuàng)建對(duì)象的線程是什么。這包括原始類型的對(duì)象版本(如字節(jié)、整數(shù)、Long等等)。如果一個(gè)對(duì)象被創(chuàng)建并分配給一個(gè)局部變量,或者作為另一個(gè)對(duì)象的成員變量創(chuàng)建,對(duì)象仍然存儲(chǔ)在堆中,這無關(guān)緊要。

? ? ? ? ? 下面是一個(gè)圖表,說明了在線程堆棧上存儲(chǔ)的調(diào)用堆棧和本地變量,以及存儲(chǔ)在堆上的對(duì)象:

? ? ? ? 局部變量可能是一種基本數(shù)據(jù)類型,在這種情況下,它完全被放在線程棧中。

? ? ? ? 局部變量也可能是一個(gè)對(duì)象的引用。在這種情況下,引用(本地變量)存儲(chǔ)在線程棧中,但是對(duì)象本身存儲(chǔ)在堆上。

? ? ? ? 對(duì)象可能包含方法,而這些方法可能包含局部變量。這些局部變量也存儲(chǔ)在線程棧中,即使方法所屬的對(duì)象存儲(chǔ)在堆上。

? ? ? ? 對(duì)象的成員變量和對(duì)象本身一起存儲(chǔ)在堆中.

? ? ? ? 靜態(tài)類變量也與類定義一起存儲(chǔ)在堆中。

? ? ? ? 堆上的對(duì)象可以被所有具有引用對(duì)象的線程訪問。當(dāng)一個(gè)線程訪問一個(gè)對(duì)象 時(shí),它也可以訪問該對(duì)象的成員變量。如果兩個(gè)線程同時(shí)調(diào)用同一個(gè)對(duì)象上的方法,那么它們都可以訪問對(duì)象的成員變量,但是每個(gè)線程都有自己的本地變量的副本。

? ? ? ? 以下的圖解說明上面的幾點(diǎn)

兩個(gè)線程有一組本地變量,局部變量(局部變量2)指向堆上的一個(gè)共享對(duì)象(對(duì)象3),這兩個(gè)線程對(duì)同一對(duì)象有不同的引用。
它們的引用是本地變量,因此存儲(chǔ)在每個(gè)線程的線程堆棧中(在每個(gè)線程堆棧中),不過,這兩個(gè)不同的引用指向堆上的同一個(gè)對(duì)象。注意,共享對(duì)象(對(duì)象3)是如何引用對(duì)象2和對(duì)象4作為成員變量的。

上圖也給我們展示了本地變量同時(shí)指向堆中的兩個(gè)不同對(duì)象(如variable1 指向堆上的兩個(gè)不同對(duì)象Object1,Object2),理論上如果線程都引用這兩個(gè)對(duì)象,他們都可以訪問這兩個(gè)對(duì)象的。但是在上圖中,每個(gè)線程只對(duì)這兩個(gè)對(duì)象的其中一個(gè)有引用。

因此,什么樣的代碼會(huì)出現(xiàn)上面的內(nèi)存圖呢?下面的代碼非常簡單的展示了這個(gè)問題:

public class MyRunnable implements Runnable(){

? ? ? public void run(){

? ? ? ? ? ? methodOne();

? ? ? }

? ? ? public void methodOne(){

? ? ? ? ? int localVariablel = 45;

? ? ? ? ? MySharedObject localVariable2 = MySharedObject.sharedInstance;

? ? ? ? ? //... do more with local variables.

? ? ? ? ? methodTwo();

? ? ? }

? ? ? public void methodTwo(){

? ? ? ? ? Integer localVariablel = 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();

? ? ? ? // meber variables pointing to two objects on the heap

? ? ? ? public Integer object2 = new Integer(22);

? ? ? ? public Integer 4 = new Integer(44);

? ? ? ? public long member1 = 12345;

? ? ? ? public long member2 = 67890;

}

如果兩個(gè)線程執(zhí)行run()方法,將會(huì)顯示前面的結(jié)果,run()方法調(diào)用methodOne()方法,methodOne()方法又調(diào)用methodTwo()方法。methodOne()聲明了一個(gè)私有的本地變量(int類型的localVariable1)和一個(gè)本地變量引用localVariable2。每個(gè)線程執(zhí)行methodOne()將會(huì)在自己的線程棧中復(fù)制一份localVariable1和localVariable2,localVariable1將會(huì)與其他完全分離,只會(huì)生存在他們自己的線程棧上,一個(gè)線程不能看到另外的線程對(duì)localVariable1的更改。每個(gè)線程在執(zhí)行methodOne()時(shí)也會(huì)復(fù)制一個(gè)localVariable2,但是最終這兩個(gè)復(fù)制變量都最終指向堆上的同一個(gè)對(duì)象。

? ? ? 代碼將localVariable2設(shè)置為指向一個(gè)靜態(tài)變量引用的對(duì)象。只有一個(gè)靜態(tài)變量的副本,這個(gè)副本存儲(chǔ)在堆中。因此,localVariable2的兩個(gè)副本都指向了靜態(tài)變量指向的MySharedObject的同一個(gè)實(shí)例。mysharedobtintinstance也被存儲(chǔ)在堆中。它對(duì)應(yīng)于上面的圖中的對(duì)象3。

注意,MySharedObject類也包括兩個(gè)成員變量,成員變量和類一樣存儲(chǔ)在堆上。這兩個(gè)成員變量指向兩個(gè)Integer對(duì)象,這些整數(shù)對(duì)象對(duì)應(yīng)上圖的Object2和Object2.注意method2()如何創(chuàng)建本地變量localVariable1,localVariable1是對(duì)一個(gè)Integer對(duì)象的引用。

methodTwo()方法創(chuàng)建了一個(gè)名為localVariable1的本地變量,這個(gè)變量引用一個(gè)Integer對(duì)象,這個(gè)方法把localVariable1的引用指向一個(gè)Integer實(shí)例,每個(gè)線程執(zhí)行methodTwo()時(shí)都會(huì)存儲(chǔ)一個(gè)localVariable1的引用副本,實(shí)例化的兩個(gè)整數(shù)對(duì)象將被存儲(chǔ)在堆中,但是由于該方法每次執(zhí)行該方法時(shí)都會(huì)創(chuàng)建一個(gè)新的整數(shù)對(duì)象,因此執(zhí)行該方法的兩個(gè)線程將創(chuàng)建單獨(dú)的整數(shù)實(shí)例。

在method2()中創(chuàng)建的整數(shù)對(duì)象對(duì)應(yīng)于上面的圖中的對(duì)象1和對(duì)象5。

還要注意MySharedObject中的類型為long的兩個(gè)成員變量,這是一個(gè)基本類型。由于這些變量是成員變量,所以它們?nèi)匀慌c對(duì)象一起存儲(chǔ)在堆中。只有本地變量存儲(chǔ)在線程堆棧中。

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

? ? ? 現(xiàn)代的硬件內(nèi)存體系結(jié)構(gòu)與內(nèi)部Java內(nèi)存模型有些不同。為了理解Java內(nèi)存模型是如何工作的,理解硬件內(nèi)存架構(gòu)也是很重要的。本節(jié)描述通用的硬件內(nèi)存架構(gòu),后面的部分將描述Java內(nèi)存模型是如何工作的。

下面是現(xiàn)代計(jì)算機(jī)硬件架構(gòu)的簡化圖

? ? ? 現(xiàn)代計(jì)算機(jī)通常有2個(gè)或更多的cpu。其中的一些cpu也可能有多個(gè)內(nèi)核。需要指出的是,在一臺(tái)擁有2個(gè)或更多cpu的現(xiàn)代計(jì)算機(jī)上,可以同時(shí)運(yùn)行多個(gè)線程。每個(gè)CPU都可以在任何給定的時(shí)間運(yùn)行一個(gè)線程。這意味著,如果您的Java應(yīng)用程序是多線程的,在你的Java應(yīng)用程序中每個(gè)CPU都可以同時(shí)運(yùn)行一個(gè)線程(并發(fā))。

? ? ? 每個(gè)CPU都包含一組寄存器,它們本質(zhì)上是CPU內(nèi)存,CPU在這些寄存器上執(zhí)行操作的速度要比在主內(nèi)存中執(zhí)行的速度快得多,這是因?yàn)镃PU能夠訪問這些寄存器的速度比它訪問主存的速度快得多。

? ? ? ? 每個(gè)CPU也可能有一個(gè)CPU緩存。事實(shí)上,大多數(shù)現(xiàn)代的cpu都有一個(gè)一定大小的CPU緩存。CPU可以比主內(nèi)存更快地訪問它的緩存內(nèi)存,但是通常不像它能夠訪問它的內(nèi)部寄存器那樣快。因此,CPU緩存在內(nèi)部寄存器和主內(nèi)存之間的速度之間。一些cpu可能有多個(gè)緩存層(1級(jí)緩存、2級(jí)緩存 ),但是這與了解Java內(nèi)存模型如何與內(nèi)存交互是不重要的。重要的是要知道,cpu可以有某種類型的緩存內(nèi)存層。

? ? ? 計(jì)算機(jī)還包含一個(gè)主要的內(nèi)存區(qū)域(RAM)。所有的cpu都可以訪問主內(nèi)存。主內(nèi)存區(qū)域通常比cpu的緩存內(nèi)存大得多。

? ? ? 當(dāng)一個(gè)CPU需要訪問主存時(shí),它將把主內(nèi)存的一部分讀到它的CPU緩存中。它甚至可以將緩存的一部分讀取到內(nèi)部寄存器中,然后對(duì)其執(zhí)行操作。當(dāng)CPU需要將結(jié)果寫回主存時(shí),它會(huì)將其內(nèi)部寄存器中的值刷新到緩存內(nèi)存中,并且在某個(gè)時(shí)候?qū)⒅邓⑿碌街鲀?nèi)存中。

連接Java內(nèi)存模型和硬件內(nèi)存架構(gòu)之間的差距

正如前面提到的,Java內(nèi)存模型和硬件內(nèi)存架構(gòu)是不同的。硬件內(nèi)存體系結(jié)構(gòu)不區(qū)分線程堆棧和堆。在硬件上,線程堆棧和堆都位于主內(nèi)存中。線程棧和堆的某些部分有時(shí)可能出現(xiàn)在CPU緩存和內(nèi)部CPU寄存器中。這張圖中有這樣的例子:


當(dāng)對(duì)象和變量可以存儲(chǔ)在計(jì)算機(jī)中不同的內(nèi)存區(qū)域時(shí),可能會(huì)出現(xiàn)某些問題。兩個(gè)主要問題是:

? ? ? 1、線程更新(寫)到共享變量的可見性

? ? ? 2、閱讀、檢查和寫入共享變量時(shí)的競態(tài)條件。

這兩個(gè)問題都將在下面的部分中解釋

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

如果兩個(gè)或多個(gè)線程共享一個(gè)對(duì)象,如果不正確使用volatile聲明或同步,那么對(duì)一個(gè)線程所做的共享對(duì)象的更新可能對(duì)其他線程來說是不可見的。

假設(shè)共享對(duì)象最初存儲(chǔ)在主內(nèi)存中。在CPU上運(yùn)行的線程會(huì)將共享對(duì)象讀取到它的CPU緩存中。在那里,它對(duì)共享對(duì)象進(jìn)行了更改。只要CPU緩存沒有被刷新到主存,那么共享對(duì)象的更改版本就不會(huì)被運(yùn)行在其他CPU上的線程所看到。這樣,每個(gè)線程都可以使用自己的共享對(duì)象副本,每個(gè)副本都位于不同的CPU緩存中

下圖演示了所描繪的場景。在左側(cè)CPU上運(yùn)行的一個(gè)線程將共享對(duì)象復(fù)制到它的CPU緩存中,并將它的count變量更改為2。對(duì)于在正確的CPU上運(yùn)行的其他線程來說,這個(gè)更改是不可見的,因?yàn)楦掠?jì)數(shù)還沒有被刷新到主內(nèi)存中。


要解決這個(gè)問題,您可以使用Java的volatile關(guān)鍵字。volatile關(guān)鍵字可以確保從主內(nèi)存直接讀取給定的變量,并在更新時(shí)將其寫入主內(nèi)存。

競態(tài)條件

如果兩個(gè)或多個(gè)線程共享一個(gè)對(duì)象,并且多個(gè)線程在該共享對(duì)象中更新變量,那么可能會(huì)出現(xiàn)競態(tài)條件。想象一下,如果線程A讀取一個(gè)共享對(duì)象的變量計(jì)數(shù)到它的CPU緩存中。想象一下,線程B也一樣,但是進(jìn)入不同的CPU緩存?,F(xiàn)在線程A添加了一個(gè)計(jì)數(shù),而線程B也做了相同的工作?,F(xiàn)在,var1已經(jīng)在每個(gè)CPU緩存中增加了兩次。

如果這些增量是按順序執(zhí)行的,那么變量計(jì)數(shù)將會(huì)增加兩次,并將原來的值+2寫回主存,然而,這兩個(gè)增量在沒有適當(dāng)同步的情況下同時(shí)進(jìn)行。不管線程A和B將其更新后的計(jì)數(shù)寫回主存,更新后的值只會(huì)比原來的值高1,盡管有兩個(gè)增量。

這張圖說明了上面描述的競態(tài)條件的問題:


要解決這個(gè)問題,您可以使用Java同步塊。同步塊保證在任何給定的時(shí)間內(nèi)只有一個(gè)線程可以進(jìn)入給定的關(guān)鍵部分。同步塊也保證在同步塊中訪問的所有變量都將從主內(nèi)存中讀取,當(dāng)線程退出同步塊時(shí),所有更新的變量將再次被刷新回主存,不管變量是否被聲明為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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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