Java內(nèi)存模型規(guī)范了Java虛擬機(jī)與計(jì)算機(jī)內(nèi)存是如何協(xié)同工作的驱闷。Java虛擬機(jī)是一個(gè)完整的計(jì)算機(jī)的一個(gè)模型,因此這個(gè)模型自然也包含一個(gè)內(nèi)存模型——又稱(chēng)為Java內(nèi)存模型。
如果你想設(shè)計(jì)表現(xiàn)良好的并發(fā)程序则酝,理解Java內(nèi)存模型是非常重要的。Java內(nèi)存模型規(guī)定了如何和何時(shí)可以看到由其他線程修改過(guò)后的共享變量的值魄梯,以及在必須時(shí)如何同步的訪問(wèn)共享變量。
原始的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è)線程僅能訪問(wèn)自己的線程棧。一個(gè)線程創(chuàng)建的本地變量對(duì)其它線程不可見(jiàn)狭园,僅自己可見(jiàn)读处。即使兩個(gè)線程執(zhí)行同樣的代碼,這兩個(gè)線程任然在在自己的線程棧中的代碼來(lái)創(chuàng)建本地變量唱矛。因此罚舱,每個(gè)線程擁有每個(gè)本地變量的獨(dú)有版本。
所有原始類(lèi)型的本地變量都存放在線程棧上绎谦,因此對(duì)其它線程不可見(jiàn)管闷。一個(gè)線程可能向另一個(gè)線程傳遞一個(gè)原始類(lèi)型變量的拷貝,但是它不能共享這個(gè)原始類(lèi)型變量自身窃肠。
堆上包含在Java程序中創(chuàng)建的所有對(duì)象包个,無(wú)論是哪一個(gè)對(duì)象創(chuàng)建的。這包括原始類(lèi)型的對(duì)象版本冤留。如果一個(gè)對(duì)象被創(chuàng)建然后賦值給一個(gè)局部變量碧囊,或者用來(lái)作為另一個(gè)對(duì)象的成員變量树灶,這個(gè)對(duì)象任然是存放在堆上。
下面這張圖演示了調(diào)用棧和本地變量存放在線程棧上糯而,對(duì)象存放在堆上天通。
一個(gè)本地變量可能是原始類(lèi)型,在這種情況下熄驼,它總是“呆在”線程棧上像寒。
一個(gè)本地變量也可能是指向一個(gè)對(duì)象的一個(gè)引用。在這種情況下瓜贾,引用(這個(gè)本地變量)存放在線程棧上诺祸,但是對(duì)象本身存放在堆上。
一個(gè)對(duì)象可能包含方法阐虚,這些方法可能包含本地變量序臂。這些本地變量任然存放在線程棧上,即使這些方法所屬的對(duì)象存放在堆上实束。
一個(gè)對(duì)象的成員變量可能隨著這個(gè)對(duì)象自身存放在堆上奥秆。不管這個(gè)成員變量是原始類(lèi)型還是引用類(lèi)型。
靜態(tài)成員變量跟隨著類(lèi)定義一起也存放在堆上咸灿。
存放在堆上的對(duì)象可以被所有持有對(duì)這個(gè)對(duì)象引用的線程訪問(wèn)构订。當(dāng)一個(gè)線程可以訪問(wèn)一個(gè)對(duì)象時(shí),它也可以訪問(wèn)這個(gè)對(duì)象的成員變量避矢。如果兩個(gè)線程同時(shí)調(diào)用同一個(gè)對(duì)象上的同一個(gè)方法悼瘾,它們將會(huì)都訪問(wèn)這個(gè)對(duì)象的成員變量,但是每一個(gè)線程都擁有這個(gè)本地變量的私有拷貝审胸。
下圖演示了上面提到的點(diǎn):
兩個(gè)線程擁有一些列的本地變量亥宿。其中一個(gè)本地變量(Local Variable 2)執(zhí)行堆上的一個(gè)共享對(duì)象(Object 3)。這兩個(gè)線程分別擁有同一個(gè)對(duì)象的不同引用砂沛。這些引用都是本地變量烫扼,因此存放在各自線程的線程棧上。這兩個(gè)不同的引用指向堆上同一個(gè)對(duì)象碍庵。
注意映企,這個(gè)共享對(duì)象(Object 3)持有Object2和Object4一個(gè)引用作為其成員變量(如圖中Object3指向Object2和Object4的箭頭)。通過(guò)在Object3中這些成員變量引用静浴,這兩個(gè)線程就可以訪問(wèn)Object2和Object4堰氓。
這張圖也展示了指向堆上兩個(gè)不同對(duì)象的一個(gè)本地變量。在這種情況下苹享,指向兩個(gè)不同對(duì)象的引用不是同一個(gè)對(duì)象双絮。理論上,兩個(gè)線程都可以訪問(wèn)Object1和Object5,如果兩個(gè)線程都擁有兩個(gè)對(duì)象的引用掷邦。但是在上圖中白胀,每一個(gè)線程僅有一個(gè)引用指向兩個(gè)對(duì)象其中之一。
因此抚岗,什么類(lèi)型的Java代碼會(huì)導(dǎo)致上面的內(nèi)存圖呢或杠?如下所示:
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è)線程同時(shí)執(zhí)行run()方法,就會(huì)出現(xiàn)上圖所示的情景宣蔚。run()方法調(diào)用methodOne()方法向抢,methodOne()調(diào)用methodTwo()方法。
methodOne()聲明了一個(gè)原始類(lèi)型的本地變量和一個(gè)引用類(lèi)型的本地變量胚委。
每個(gè)線程執(zhí)行methodOne()都會(huì)在它們對(duì)應(yīng)的線程棧上創(chuàng)建localVariable1和localVariable2的私有拷貝挟鸠。localVariable1變量彼此完全獨(dú)立,僅“生活”在每個(gè)線程的線程棧上亩冬。一個(gè)線程看不到另一個(gè)線程對(duì)它的localVariable1私有拷貝做出的修改艘希。
每個(gè)線程執(zhí)行methodOne()時(shí)也將會(huì)創(chuàng)建它們各自的localVariable2拷貝。然而硅急,兩個(gè)localVariable2的不同拷貝都指向堆上的同一個(gè)對(duì)象覆享。代碼中通過(guò)一個(gè)靜態(tài)變量設(shè)置localVariable2指向一個(gè)對(duì)象引用。僅存在一個(gè)靜態(tài)變量的一份拷貝营袜,這份拷貝存放在堆上撒顿。因此,localVariable2的兩份拷貝都指向由MySharedObject指向的靜態(tài)變量的同一個(gè)實(shí)例荚板。MySharedObject實(shí)例也存放在堆上凤壁。它對(duì)應(yīng)于上圖中的Object3。
注意跪另,MySharedObject類(lèi)也包含兩個(gè)成員變量拧抖。這些成員變量隨著這個(gè)對(duì)象存放在堆上。這兩個(gè)成員變量指向另外兩個(gè)Integer對(duì)象免绿。這些Integer對(duì)象對(duì)應(yīng)于上圖中的Object2和Object4.
注意徙鱼,methodTwo()創(chuàng)建一個(gè)名為localVariable的本地變量。這個(gè)成員變量是一個(gè)指向一個(gè)Integer對(duì)象的對(duì)象引用针姿。這個(gè)方法設(shè)置localVariable1引用指向一個(gè)新的Integer實(shí)例。在執(zhí)行methodTwo方法時(shí)厌衙,localVariable1引用將會(huì)在每個(gè)線程中存放一份拷貝距淫。這兩個(gè)Integer對(duì)象實(shí)例化將會(huì)被存儲(chǔ)堆上,但是每次執(zhí)行這個(gè)方法時(shí)婶希,這個(gè)方法都會(huì)創(chuàng)建一個(gè)新的Integer對(duì)象榕暇,兩個(gè)線程執(zhí)行這個(gè)方法將會(huì)創(chuàng)建兩個(gè)不同的Integer實(shí)例。methodTwo方法創(chuàng)建的Integer對(duì)象對(duì)應(yīng)于上圖中的Object1和Object5。
還有一點(diǎn)彤枢,MySharedObject類(lèi)中的兩個(gè)long類(lèi)型的成員變量是原始類(lèi)型的狰晚。因?yàn)椋@些變量是成員變量缴啡,所以它們?nèi)稳浑S著該對(duì)象存放在堆上壁晒,僅有本地變量存放在線程棧上。
硬件內(nèi)存架構(gòu)
現(xiàn)代硬件內(nèi)存模型與Java內(nèi)存模型有一些不同业栅。理解內(nèi)存模型架構(gòu)以及Java內(nèi)存模型如何與它協(xié)同工作也是非常重要的秒咐。這部分描述了通用的硬件內(nèi)存架構(gòu),下面的部分將會(huì)描述Java內(nèi)存是如何與它“聯(lián)手”工作的碘裕。
下面是現(xiàn)代計(jì)算機(jī)硬件架構(gòu)的簡(jiǎn)單圖示:
一個(gè)現(xiàn)代計(jì)算機(jī)通常由兩個(gè)或者多個(gè)CPU携取。其中一些CPU還有多核。從這一點(diǎn)可以看出帮孔,在一個(gè)有兩個(gè)或者多個(gè)CPU的現(xiàn)代計(jì)算機(jī)上同時(shí)運(yùn)行多個(gè)線程是可能的雷滋。每個(gè)CPU在某一時(shí)刻運(yùn)行一個(gè)線程是沒(méi)有問(wèn)題的。這意味著文兢,如果你的Java程序是多線程的晤斩,在你的Java程序中每個(gè)CPU上一個(gè)線程可能同時(shí)(并發(fā))執(zhí)行。
每個(gè)CPU都包含一系列的寄存器禽作,它們是CPU內(nèi)內(nèi)存的基礎(chǔ)尸昧。CPU在寄存器上執(zhí)行操作的速度遠(yuǎn)大于在主存上執(zhí)行的速度。這是因?yàn)镃PU訪問(wèn)寄存器的速度遠(yuǎn)大于主存旷偿。
每個(gè)CPU可能還有一個(gè)CPU緩存層烹俗。實(shí)際上,絕大多數(shù)的現(xiàn)代CPU都有一定大小的緩存層萍程。CPU訪問(wèn)緩存層的速度快于訪問(wèn)主存的速度幢妄,但通常比訪問(wèn)內(nèi)部寄存器的速度還要慢一點(diǎn)。一些CPU還有多層緩存茫负,但這些對(duì)理解Java內(nèi)存模型如何和內(nèi)存交互不是那么重要蕉鸳。只要知道CPU中可以有一個(gè)緩存層就可以了。
一個(gè)計(jì)算機(jī)還包含一個(gè)主存忍法。所有的CPU都可以訪問(wèn)主存潮尝。主存通常比CPU中的緩存大得多。
通常情況下饿序,當(dāng)一個(gè)CPU需要讀取主存時(shí)勉失,它會(huì)將主存的部分讀到CPU緩存中。它甚至可能將緩存中的部分內(nèi)容讀到它的內(nèi)部寄存器中原探,然后在寄存器中執(zhí)行操作乱凿。當(dāng)CPU需要將結(jié)果寫(xiě)回到主存中去時(shí)顽素,它會(huì)將內(nèi)部寄存器的值刷新到緩存中,然后在某個(gè)時(shí)間點(diǎn)將值刷新回主存徒蟆。
當(dāng)CPU需要在緩存層存放一些東西的時(shí)候胁出,存放在緩存中的內(nèi)容通常會(huì)被刷新回主存。CPU緩存可以在某一時(shí)刻將數(shù)據(jù)局部寫(xiě)到它的內(nèi)存中段审,和在某一時(shí)刻局部刷新它的內(nèi)存全蝶。它不會(huì)再某一時(shí)刻讀/寫(xiě)整個(gè)緩存。通常戚哎,在一個(gè)被稱(chēng)作“cache lines”的更小的內(nèi)存塊中緩存被更新裸诽。一個(gè)或者多個(gè)緩存行可能被讀到緩存,一個(gè)或者多個(gè)緩存行可能再被刷新回主存型凳。
Java內(nèi)存模型和硬件內(nèi)存架構(gòu)之間的橋接
上面已經(jīng)提到丈冬,Java內(nèi)存模型與硬件內(nèi)存架構(gòu)之間存在差異。硬件內(nèi)存架構(gòu)沒(méi)有區(qū)分線程棧和堆甘畅。對(duì)于硬件埂蕊,所有的線程棧和堆都分布在主內(nèi)中。部分線程棧和堆可能有時(shí)候會(huì)出現(xiàn)在CPU緩存中和CPU內(nèi)部的寄存器中疏唾。如下圖所示:
當(dāng)對(duì)象和變量被存放在計(jì)算機(jī)中各種不同的內(nèi)存區(qū)域中時(shí)蓄氧,就可能會(huì)出現(xiàn)一些具體的問(wèn)題。主要包括如下兩個(gè)方面:
-線程對(duì)共享變量修改的可見(jiàn)性
-當(dāng)讀槐脏,寫(xiě)和檢查共享變量時(shí)出現(xiàn)race conditions
下面我們專(zhuān)門(mén)來(lái)解釋以下這兩個(gè)問(wèn)題喉童。
共享對(duì)象可見(jiàn)性
如果兩個(gè)或者更多的線程在沒(méi)有正確的使用volatile聲明或者同步的情況下共享一個(gè)對(duì)象,一個(gè)線程更新這個(gè)共享對(duì)象可能對(duì)其它線程來(lái)說(shuō)是不接見(jiàn)的顿天。
想象一下堂氯,共享對(duì)象被初始化在主存中。跑在CPU上的一個(gè)線程將這個(gè)共享對(duì)象讀到CPU緩存中牌废。然后修改了這個(gè)對(duì)象咽白。只要CPU緩存沒(méi)有被刷新會(huì)主存,對(duì)象修改后的版本對(duì)跑在其它CPU上的線程都是不可見(jiàn)的鸟缕。這種方式可能導(dǎo)致每個(gè)線程擁有這個(gè)共享對(duì)象的私有拷貝晶框,每個(gè)拷貝停留在不同的CPU緩存中。
下圖示意了這種情形懂从。跑在左邊CPU的線程拷貝這個(gè)共享對(duì)象到它的CPU緩存中授段,然后將count變量的值修改為2。這個(gè)修改對(duì)跑在右邊CPU上的其它線程是不可見(jiàn)的番甩,因?yàn)樾薷暮蟮腸ount的值還沒(méi)有被刷新回主存中去畴蒲。
解決這個(gè)問(wèn)題你可以使用Java中的volatile
關(guān)鍵字。volatile
關(guān)鍵字可以保證直接從主存中讀取一個(gè)變量对室,如果這個(gè)變量被修改后模燥,總是會(huì)被寫(xiě)回到主存中去。
Race Conditions
如果兩個(gè)或者更多的線程共享一個(gè)對(duì)象掩宜,多個(gè)線程在這個(gè)共享對(duì)象上更新變量蔫骂,就有可能發(fā)生race conditions。
想象一下牺汤,如果線程A讀一個(gè)共享對(duì)象的變量count到它的CPU緩存中辽旋。
再想象一下,線程B也做了同樣的事情檐迟,但是往一個(gè)不同的CPU緩存中补胚。
現(xiàn)在線程A將count加1,線程B也做了同樣的事情∽烦伲現(xiàn)在count已經(jīng)被增加了兩個(gè)溶其,每個(gè)CPU緩存中一次。
如果這些增加操作被順序的執(zhí)行敦间,變量count應(yīng)該被增加兩次瓶逃,然后原值+2被寫(xiě)回到主存中去。
然而廓块,兩次增加都是在沒(méi)有適當(dāng)?shù)耐较虏l(fā)執(zhí)行的厢绝。無(wú)論是線程A還是線程B將count修改后的版本寫(xiě)回到主存中去,修改后的值僅會(huì)比原值大1带猴,盡管增加了兩次昔汉。
下圖演示了上面描述的情況:
解決這個(gè)問(wèn)題可以使用Java同步塊。一個(gè)同步塊可以保證在同一時(shí)刻僅有一個(gè)線程可以進(jìn)入代碼的臨界區(qū)拴清。同步塊還可以保證代碼塊中所有被訪問(wèn)的變量將會(huì)從主存中讀入靶病,當(dāng)線程退出同步代碼塊時(shí),所有被更新的變量都會(huì)被刷新回主存中去贷掖,不管這個(gè)變量是否被聲明為volatile嫡秕。