Java 內(nèi)存模式
Java內(nèi)存模型規(guī)范了Java虛擬機(jī)與計(jì)算機(jī)內(nèi)存是如何協(xié)同工作的。Java虛擬機(jī)是一個(gè)完整的計(jì)算機(jī)的一個(gè)模型,因此這個(gè)模型自然也包含一個(gè)內(nèi)存模型——又稱為Java內(nèi)存模型桌吃。
如果你想設(shè)計(jì)表現(xiàn)良好的并發(fā)程序,理解Java內(nèi)存模型是非常重要的。Java內(nèi)存模型規(guī)定了如何和何時(shí)可以看到由其他線程修改過后的共享變量的值筐咧,以及在必須時(shí)如何同步的訪問共享變量
原始的Java內(nèi)存模型存在一些不足仔掸,因此Java內(nèi)存模型在Java1.5時(shí)被重新修訂脆贵。這個(gè)版本的Java內(nèi)存模型在Java8中人在使用。
Java內(nèi)存模型內(nèi)部原理
Java內(nèi)存模型把Java虛擬機(jī)內(nèi)部劃分為線程棧和堆起暮。這張圖演示了Java內(nèi)存模型的邏輯視圖
每一個(gè)運(yùn)行在Java虛擬機(jī)里的線程都擁有自己的線程棧卖氨。這個(gè)線程棧包含了這個(gè)線程調(diào)用的方法當(dāng)前執(zhí)行點(diǎn)相關(guān)的信息。一個(gè)線程僅能訪問自己的線程棧负懦。一個(gè)線程創(chuàng)建的本地變量對(duì)其它線程不可見筒捺,僅自己可見。即使兩個(gè)線程執(zhí)行同樣的代碼纸厉,這兩個(gè)線程任然在在自己的線程棧中的代碼來創(chuàng)建本地變量系吭。因此,每個(gè)線程擁有每個(gè)本地變量的獨(dú)有版本残腌。
線程堆棧還包含執(zhí)行每個(gè)方法的所有局部變量(調(diào)用堆棧上的所有方法)村斟。 一個(gè)線程只能訪問它自己的線程堆棧。 線程所創(chuàng)建的局部變量對(duì)于所有其他線程都是不可見的抛猫,而不是創(chuàng)建線程的線程蟆盹。 即使兩個(gè)線程執(zhí)行完全相同的代碼,兩個(gè)線程仍然會(huì)在每個(gè)自己的線程堆棧中創(chuàng)建該代碼的局部變量闺金。 因此逾滥,每個(gè)線程都有自己的每個(gè)局部變量的版本。
原始類型(boolean败匹,byte寨昙,short,char掀亩,int舔哪,long,float槽棍,double)的所有局部變量都完全存儲(chǔ)在線程堆棧上捉蚤,因此對(duì)其他線程不可見。 一個(gè)線程可以將一個(gè)原始類型變量的副本傳遞給另一個(gè)線程炼七,但是它不能共享原始局部變量本身缆巧。
該堆包含您的Java應(yīng)用程序中創(chuàng)建的所有對(duì)象,無論創(chuàng)建對(duì)象的線程如何豌拙。 這包括基本類型的對(duì)象版本(例如陕悬,字節(jié),整數(shù)按傅,長(zhǎng)等)捉超。 如果對(duì)象被創(chuàng)建并分配給局部變量胧卤,或者創(chuàng)建為另一個(gè)對(duì)象的成員變量,對(duì)象仍然存儲(chǔ)在堆上拼岳,這并不重要灌侣。
下面的圖示出了存儲(chǔ)在線程堆棧上的調(diào)用堆棧和局部變量以及存儲(chǔ)在堆上的對(duì)象:
局部變量可能是一個(gè)原始類型,在這種情況下裂问,它完全保留在線程棧上侧啼。
局部變量也可以是對(duì)象的引用。在這種情況下堪簿,引用(局部變量)存儲(chǔ)在線程堆棧上痊乾,但對(duì)象本身存儲(chǔ)在堆上。
對(duì)象可能包含方法椭更,這些方法可能包含局部變量哪审。這些局部變量也存儲(chǔ)在線程堆棧中,即使該方法所屬的對(duì)象存儲(chǔ)在堆上虑瀑。
對(duì)象的成員變量與對(duì)象本身一起存儲(chǔ)在堆上湿滓。當(dāng)成員變量是原始類型,并且它是對(duì)對(duì)象的引用時(shí)舌狗,這是真的叽奥。
靜態(tài)類變量也與類定義一起存儲(chǔ)在堆上。
所有對(duì)該對(duì)象引用的線程都可以訪問堆上的對(duì)象痛侍。當(dāng)線程訪問對(duì)象時(shí)朝氓,它也可以訪問對(duì)象的成員變量。如果兩個(gè)線程同時(shí)在同一個(gè)對(duì)象上調(diào)用一個(gè)方法主届,那么它們都可以訪問對(duì)象的成員變量赵哲,但每個(gè)線程都有自己的局部變量副本。
[圖片上傳失敗...(image-f8c969-1529157793436)]
兩個(gè)線程有??一組局部變量君丁。局部變量(Local Variable 2)之一指向堆上的共享對(duì)象(對(duì)象3)枫夺。兩個(gè)線程各自對(duì)同一個(gè)對(duì)象有不同的引用。它們的引用是局部變量绘闷,因此存儲(chǔ)在每個(gè)線程的線程堆棧(每個(gè))上橡庞。兩個(gè)不同的引用指向堆上的同一個(gè)對(duì)象。
注意共享對(duì)象(Object 3)如何引用Object 2和Object 4作為成員變量(由Object 3到Object 2和Object 4的箭頭所示)簸喂。通過對(duì)象3中的這些成員變量引用毙死,兩個(gè)線程可以訪問對(duì)象2和對(duì)象4燎潮。
該圖還顯示了一個(gè)局部變量喻鳄,指向堆上的兩個(gè)不同對(duì)象。在這種情況下确封,引用指向兩個(gè)不同的對(duì)象(對(duì)象1和對(duì)象5)除呵,而不是相同的對(duì)象再菊。在理論上,如果兩個(gè)線程都對(duì)兩個(gè)對(duì)象都引用颜曾,則兩個(gè)線程都可以訪問對(duì)象1和對(duì)象5纠拔。但是在上圖中,每個(gè)線程只有一個(gè)對(duì)兩個(gè)對(duì)象之一的引用泛豪。
那么稠诲,什么樣的Java代碼可以導(dǎo)致上述內(nèi)存圖?那么代碼如下代碼一樣簡(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()方法調(diào)用methodOne()和methodOne()調(diào)用methodTwo()。
methodOne()聲明一個(gè)原始局部變量(int類型的localVariable1)和一個(gè)作為對(duì)象引用(localVariable2)的局部變量价卤。
執(zhí)行methodOne()的每個(gè)線程將在它們各自的線程堆棧上創(chuàng)建自己的localVariable1和localVariable2副本劝萤。 localVariable1變量將完全分開,只能生活在每個(gè)線程的線程堆棧上慎璧。一個(gè)線程無法看到另一個(gè)線程對(duì)其localVariable1的副本進(jìn)行了什么更改床嫌。
執(zhí)行methodOne()的每個(gè)線程也將創(chuàng)建自己的localVariable2副本。然而胸私,localVariable2的兩個(gè)不同的副本都最終指向堆上的同一個(gè)對(duì)象厌处。代碼將localVariable2設(shè)置為指向靜態(tài)變量引用的對(duì)象。靜態(tài)變量只有一個(gè)副本岁疼,該副本存儲(chǔ)在堆上嘱蛋。因此,localVariable2的兩個(gè)副本都指向靜態(tài)變量指向的MySharedObject的同一個(gè)實(shí)例五续。 MySharedObject實(shí)例也存儲(chǔ)在堆上洒敏。它對(duì)應(yīng)于上圖中的對(duì)象3。
注意MySharedObject類如何包含兩個(gè)成員變量疙驾。成員變量本身與對(duì)象一起存儲(chǔ)在堆上凶伙。兩個(gè)成員變量指向另外兩個(gè)Integer對(duì)象。這些整數(shù)對(duì)象對(duì)應(yīng)于上圖中的對(duì)象2和對(duì)象4它碎。
還要注意methodTwo()如何創(chuàng)建一個(gè)名為localVariable1的局部變量函荣。這個(gè)局部變量是一個(gè)Integer對(duì)象的對(duì)象引用。該方法將localVariable1引用設(shè)置為指向一個(gè)新的整數(shù)實(shí)例扳肛。 localVariable1引用將被存儲(chǔ)在執(zhí)行methodTwo()的每個(gè)線程的一個(gè)副本中傻挂。實(shí)例化的兩個(gè)Integer對(duì)象將被存儲(chǔ)在堆上,但是由于該方法在每次執(zhí)行該方法時(shí)都會(huì)創(chuàng)建一個(gè)新的Integer對(duì)象挖息,所以執(zhí)行此方法的兩個(gè)線程將創(chuàng)建單獨(dú)的Integer實(shí)例金拒。 methodTwo()中創(chuàng)建的Integer對(duì)象對(duì)應(yīng)于上圖中的Object 1和Object 5。
還要注意類型為long的類MySharedObject中的兩個(gè)成員變量,這是一個(gè)原始類型绪抛。由于這些變量是成員變量资铡,它們?nèi)匀慌c對(duì)象一起存儲(chǔ)在堆上。只有局部變量存儲(chǔ)在線程堆棧中
硬件內(nèi)存架構(gòu)
現(xiàn)代硬件內(nèi)存架構(gòu)與內(nèi)部Java內(nèi)存模型有所不同幢码。 了解硬件內(nèi)存架構(gòu)也很重要笤休,以了解Java內(nèi)存模型的工作原理。 本節(jié)介紹常見的硬件內(nèi)存架構(gòu)症副,后面的部分將介紹Java內(nèi)存模型的工作原理店雅。
以下是現(xiàn)代計(jì)算機(jī)硬件架構(gòu)的簡(jiǎn)化圖:
現(xiàn)代計(jì)算機(jī)通常有2個(gè)或更多的CPU。其中一些CPU也可能有多個(gè)內(nèi)核贞铣。關(guān)鍵是底洗,在具有2個(gè)或更多個(gè)CPU的現(xiàn)代計(jì)算機(jī)上,可以同時(shí)運(yùn)行多個(gè)線程咕娄。每個(gè)CPU都可以在任何給定的時(shí)間運(yùn)行一個(gè)線程亥揖。這意味著如果您的Java應(yīng)用程序是多線程的,則每個(gè)CPU可能會(huì)在Java應(yīng)用程序中同時(shí)(同時(shí))運(yùn)行一個(gè)線程圣勒。
每個(gè)CPU都包含一組本質(zhì)上是CPU內(nèi)存的寄存器费变。 CPU可以在這些寄存器上執(zhí)行的操作比在主存儲(chǔ)器中對(duì)變量執(zhí)行的操作要快得多。這是因?yàn)镃PU可以訪問這些寄存器比訪問主內(nèi)存的速度快得多圣贸。
每個(gè)CPU也可以具有CPU緩存存儲(chǔ)器層挚歧。事實(shí)上,大多數(shù)現(xiàn)代CPU具有一定大小的緩存內(nèi)存層吁峻。 CPU可以比主存儲(chǔ)器快速訪問其緩存滑负,但通常不能像訪問其內(nèi)部寄存器一樣快。因此用含,CPU緩存內(nèi)存位于內(nèi)部寄存器和主存儲(chǔ)器的速度之間矮慕。某些CPU可能有多個(gè)緩存層(1級(jí)和2級(jí)),但是要了解Java內(nèi)存模型如何與內(nèi)存進(jìn)行交互啄骇,這并不重要痴鳄。重要的是知道CPU可以具有某種緩存內(nèi)存層。
計(jì)算機(jī)還包含主存儲(chǔ)區(qū)(RAM)缸夹。所有CPU都可以訪問主存儲(chǔ)器痪寻。主存儲(chǔ)區(qū)通常遠(yuǎn)大于CPU的高速緩沖存儲(chǔ)器。
通常虽惭,當(dāng)CPU需要訪問主存儲(chǔ)器時(shí)橡类,它會(huì)將主存儲(chǔ)器的一部分讀入其CPU緩存。甚至可以將部分高速緩存讀入其內(nèi)部寄存器芽唇,然后對(duì)其執(zhí)行操作顾画。當(dāng)CPU需要將結(jié)果寫回主內(nèi)存時(shí),它將從內(nèi)部寄存器中將值刷新到高速緩沖存儲(chǔ)器,并在某些時(shí)候?qū)⒅邓⑿碌街鞔鎯?chǔ)器亲雪。
當(dāng)CPU需要在高速緩沖存儲(chǔ)器中存儲(chǔ)其他內(nèi)容時(shí),存儲(chǔ)在高速緩沖存儲(chǔ)器中的值通常被刷新回主存儲(chǔ)器疚膊。 CPU緩存一次可以將數(shù)據(jù)寫入其內(nèi)存的一部分义辕,并一次刷新其內(nèi)存的一部分。每次更新時(shí)寓盗,它不必讀取/寫入完整的緩存灌砖。通常,緩存在被稱為“高速緩存行”的更小的存儲(chǔ)塊中被更新傀蚌』裕可以將一個(gè)或多個(gè)高速緩存行讀入高速緩沖存儲(chǔ)器,并且可以將一個(gè)或多個(gè)高速緩存線重新刷回主存儲(chǔ)器善炫。
Java內(nèi)存模型與硬件內(nèi)存架構(gòu)之間的聯(lián)系
如前所述撩幽,Java內(nèi)存模型和硬件內(nèi)存架構(gòu)是不同的。 硬件內(nèi)存架構(gòu)不區(qū)分線程堆棧和堆箩艺。 在硬件上窜醉,線程堆棧和堆都位于主內(nèi)存中。 線程堆棧和堆的一部分有時(shí)可能存在于CPU高速緩存和內(nèi)部CPU寄存器中艺谆。 這在圖中說明:
當(dāng)對(duì)象和變量可以存儲(chǔ)在計(jì)算機(jī)的各種不同的存儲(chǔ)區(qū)域中時(shí)榨惰,可能會(huì)出現(xiàn)某些問題。 兩個(gè)主要問題是:
- 線程更新(寫入)到共享變量的可見性静汤。
- 讀取琅催,檢查和寫入共享變量時(shí)的競(jìng)爭(zhēng)條件
這兩個(gè)問題將在以下部分中解釋。
共享對(duì)象的可見性
如果兩個(gè)或多個(gè)線程共享一個(gè)對(duì)象虫给,沒有正確使用volatile聲明或同步藤抡,一個(gè)線程所做的共享對(duì)象的更新可能對(duì)其他線程是不可見的。
假設(shè)共享對(duì)象最初存儲(chǔ)在主內(nèi)存中抹估。 在CPU 1上運(yùn)行的線程然后將共享對(duì)象讀入其CPU緩存杰捂。 在那里它對(duì)共享對(duì)象進(jìn)行了更改。 只要CPU緩存尚未刷新到主內(nèi)存棋蚌,共享對(duì)象的更改版本對(duì)于在其他CPU上運(yùn)行的線程不可見嫁佳。 這樣一來,每個(gè)線程可能會(huì)以自己的共享對(duì)象副本結(jié)束谷暮,每個(gè)副本都坐在不同的CPU緩存中蒿往。
下圖說明了草圖的情況。 在左CPU上運(yùn)行的一個(gè)線程將共享對(duì)象復(fù)制到其CPU緩存中湿弦,并將其計(jì)數(shù)變量更改為2.對(duì)于正確CPU上運(yùn)行的其他線程瓤漏,此更改不可見,因?yàn)楦掠?jì)數(shù)尚未刷新到主 記憶還沒
要解決這個(gè)問題,你可以使用Java的volatile關(guān)鍵字蔬充。 volatile關(guān)鍵字可以確保給定的變量直接從主內(nèi)存中讀取蝶俱,并在更新時(shí)總是寫回主內(nèi)存。
注意
volatile并不能保證線程的安全饥漫,只是在某種場(chǎng)景下使用才能保證線程安全榨呆。如果有且只有一個(gè)線程在寫,其他的所有線程都在讀庸队,那么可以保證線程的安全积蜻,如果多個(gè)線程寫多個(gè)線程讀,那么不能保證線程安全
條件競(jìng)爭(zhēng)
如果兩個(gè)或多個(gè)線程共享對(duì)象彻消,并且多個(gè)線程更新該共享對(duì)象中的變量竿拆,則可能會(huì)發(fā)生競(jìng)爭(zhēng)條件。
假設(shè)線程A將共享對(duì)象的變量計(jì)數(shù)讀入其CPU緩存中宾尚。 想像一下丙笋,那個(gè)線程B也是一樣的,但進(jìn)入不同的CPU緩存煌贴。 現(xiàn)在線程A增加一個(gè)計(jì)數(shù)不见,線程B也是一樣的。 現(xiàn)在崔步,var1已經(jīng)增加了兩次稳吮,每次CPU緩存一次。
如果這些增量依次執(zhí)行井濒,則變量計(jì)數(shù)將被增加兩次灶似,并將原始值+ 2寫回到主存儲(chǔ)器。
但是瑞你,兩個(gè)增量在沒有正確同步的情況下同時(shí)進(jìn)行酪惭。 不管線程A和B哪個(gè)將其更新版本的計(jì)數(shù)寫回主內(nèi)存,盡管有兩個(gè)增量者甲,更新后的值將僅比原始值高1春感。
該圖說明了如上所述的競(jìng)爭(zhēng)條件的問題的發(fā)生:
要解決此問題,您可以使用Java同步塊虏缸。 同步塊保證在任何給定時(shí)間只有一個(gè)線程可以進(jìn)入代碼的給定關(guān)鍵部分鲫懒。 同步塊還保證在同步塊內(nèi)訪問的所有變量將從主存儲(chǔ)器讀入,并且當(dāng)線程退出同步塊時(shí)刽辙,所有更新的變量將被刷新回主存儲(chǔ)器窥岩,而不管該變量是否被聲明為volatile。