網(wǎng)上看到一片很nice的圖文并茂的技術文章柏腻,引用擴散娱节。
Java內(nèi)存模型內(nèi)部原理
每一個運行在Java虛擬機里的線程都擁有自己的線程棧彰阴。這個線程棧包含了這個線程調(diào)用的方法當前執(zhí)行點相關的信息凭涂。一個線程僅能訪問自己的線程棧被济。一個線程創(chuàng)建的本地變量對其它線程不可見救赐,僅自己可見。即使兩個線程執(zhí)行同樣的代碼只磷,這兩個線程任然在在自己的線程棧中的代碼來創(chuàng)建本地變量经磅。因此,每個線程擁有每個本地變量的獨有版本钮追。
所有原始類型的本地變量都存放在線程棧上预厌,因此對其它線程不可見。一個線程可能向另一個線程傳遞一個原始類型變量的拷貝元媚,但是它不能共享這個原始類型變量自身轧叽。
堆上包含在Java程序中創(chuàng)建的所有對象,無論是哪一個對象創(chuàng)建的刊棕。這包括原始類型的對象版本炭晒。如果一個對象被創(chuàng)建然后賦值給一個局部變量,或者用來作為另一個對象的成員變量鞠绰,這個對象任然是存放在堆上腰埂。
下面這張圖演示了調(diào)用棧和本地變量存放在線程棧上,對象存放在堆上蜈膨。
一個本地變量可能是原始類型屿笼,在這種情況下,它總是“呆在”線程棧上翁巍。
一個本地變量也可能是指向一個對象的一個引用驴一。在這種情況下,引用(這個本地變量)存放在線程棧上灶壶,但是對象本身存放在堆上肝断。
一個對象可能包含方法停做,這些方法可能包含本地變量夏块。這些本地變量任然存放在線程棧上壕鹉,即使這些方法所屬的對象存放在堆上雏赦。
一個對象的成員變量可能隨著這個對象自身存放在堆上。不管這個成員變量是原始類型還是引用類型趣钱。
靜態(tài)成員變量跟隨著類定義一起也存放在堆上涌献。
存放在堆上的對象可以被所有持有對這個對象引用的線程訪問。當一個線程可以訪問一個對象時首有,它也可以訪問這個對象的成員變量燕垃。如果兩個線程同時調(diào)用同一個對象上的同一個方法,它們將會都訪問這個對象的成員變量井联,但是每一個線程都擁有這個本地變量的私有拷貝卜壕。
兩個線程擁有一些列的本地變量。其中一個本地變量(Local Variable 2)執(zhí)行堆上的一個共享對象(Object 3)烙常。這兩個線程分別擁有同一個對象的不同引用轴捎。這些引用都是本地變量,因此存放在各自線程的線程棧上蚕脏。這兩個不同的引用指向堆上同一個對象轮蜕。
注意,這個共享對象(Object 3)持有Object2和Object4一個引用作為其成員變量(如圖中Object3指向Object2和Object4的箭頭)蝗锥。通過在Object3中這些成員變量引用,這兩個線程就可以訪問Object2和Object4率触。
這張圖也展示了指向堆上兩個不同對象的一個本地變量终议。在這種情況下,指向兩個不同對象的引用不是同一個對象葱蝗。理論上穴张,兩個線程都可以訪問Object1和Object5,如果兩個線程都擁有兩個對象的引用两曼。但是在上圖中皂甘,每一個線程僅有一個引用指向兩個對象其中之一。
- 示例代碼
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;
}
methodOne()聲明了一個原始類型的本地變量和一個引用類型的本地變量悼凑。
每個線程執(zhí)行methodOne()都會在它們對應的線程棧上創(chuàng)建localVariable1和localVariable2的私有拷貝偿枕。localVariable1變量彼此完全獨立,僅“生活”在每個線程的線程棧上户辫。一個線程看不到另一個線程對它的localVariable1私有拷貝做出的修改渐夸。
每個線程執(zhí)行methodOne()時也將會創(chuàng)建它們各自的localVariable2拷貝。然而渔欢,兩個localVariable2的不同拷貝都指向堆上的同一個對象墓塌。代碼中通過一個靜態(tài)變量設置localVariable2指向一個對象引用。僅存在一個靜態(tài)變量的一份拷貝,這份拷貝存放在堆上苫幢。因此访诱,localVariable2的兩份拷貝都指向由MySharedObject指向的靜態(tài)變量的同一個實例。MySharedObject實例也存放在堆上韩肝。它對應于上圖中的Object3触菜。
注意,MySharedObject類也包含兩個成員變量伞梯。這些成員變量隨著這個對象存放在堆上玫氢。這兩個成員變量指向另外兩個Integer對象。這些Integer對象對應于上圖中的Object2和Object4.
注意谜诫,methodTwo()創(chuàng)建一個名為localVariable的本地變量漾峡。這個成員變量是一個指向一個Integer對象的對象引用。這個方法設置localVariable1引用指向一個新的Integer實例喻旷。在執(zhí)行methodTwo方法時生逸,localVariable1引用將會在每個線程中存放一份拷貝。這兩個Integer對象實例化將會被存儲堆上且预,但是每次執(zhí)行這個方法時槽袄,這個方法都會創(chuàng)建一個新的Integer對象,兩個線程執(zhí)行這個方法將會創(chuàng)建兩個不同的Integer實例锋谐。methodTwo方法創(chuàng)建的Integer對象對應于上圖中的Object1和Object5遍尺。
還有一點,MySharedObject類中的兩個long類型的成員變量是原始類型的涮拗。因為乾戏,這些變量是成員變量,所以它們?nèi)稳浑S著該對象存放在堆上三热,僅有本地變量存放在線程棧上鼓择。
硬件內(nèi)存架構
現(xiàn)代硬件內(nèi)存模型與Java內(nèi)存模型有一些不同。理解內(nèi)存模型架構以及Java內(nèi)存模型如何與它協(xié)同工作也是非常重要的就漾。這部分描述了通用的硬件內(nèi)存架構呐能,下面的部分將會描述Java內(nèi)存是如何與它“聯(lián)手”工作的。
下面是現(xiàn)代計算機硬件架構的簡單圖示:
一個現(xiàn)代計算機通常由兩個或者多個CPU抑堡。其中一些CPU還有多核摆出。從這一點可以看出,在一個有兩個或者多個CPU的現(xiàn)代計算機上同時運行多個線程是可能的夷野。每個CPU在某一時刻運行一個線程是沒有問題的懊蒸。這意味著,如果你的Java程序是多線程的悯搔,在你的Java程序中每個CPU上一個線程可能同時(并發(fā))執(zhí)行骑丸。
每個CPU都包含一系列的寄存器舌仍,它們是CPU內(nèi)內(nèi)存的基礎。CPU在寄存器上執(zhí)行操作的速度遠大于在主存上執(zhí)行的速度通危。這是因為CPU訪問寄存器的速度遠大于主存铸豁。
每個CPU可能還有一個CPU緩存層。實際上菊碟,絕大多數(shù)的現(xiàn)代CPU都有一定大小的緩存層节芥。CPU訪問緩存層的速度快于訪問主存的速度,但通常比訪問內(nèi)部寄存器的速度還要慢一點逆害。一些CPU還有多層緩存头镊,但這些對理解Java內(nèi)存模型如何和內(nèi)存交互不是那么重要。只要知道CPU中可以有一個緩存層就可以了魄幕。
一個計算機還包含一個主存相艇。所有的CPU都可以訪問主存。主存通常比CPU中的緩存大得多纯陨。
通常情況下坛芽,當一個CPU需要讀取主存時,它會將主存的部分讀到CPU緩存中翼抠。它甚至可能將緩存中的部分內(nèi)容讀到它的內(nèi)部寄存器中咙轩,然后在寄存器中執(zhí)行操作。當CPU需要將結果寫回到主存中去時阴颖,它會將內(nèi)部寄存器的值刷新到緩存中活喊,然后在某個時間點將值刷新回主存。
當CPU需要在緩存層存放一些東西的時候量愧,存放在緩存中的內(nèi)容通常會被刷新回主存胧弛。CPU緩存可以在某一時刻將數(shù)據(jù)局部寫到它的內(nèi)存中,和在某一時刻局部刷新它的內(nèi)存侠畔。它不會再某一時刻讀/寫整個緩存。通常损晤,在一個被稱作“cache lines”的更小的內(nèi)存塊中緩存被更新软棺。一個或者多個緩存行可能被讀到緩存,一個或者多個緩存行可能再被刷新回主存尤勋。
Java內(nèi)存模型和硬件內(nèi)存架構之間的橋接
上面已經(jīng)提到喘落,Java內(nèi)存模型與硬件內(nèi)存架構之間存在差異。硬件內(nèi)存架構沒有區(qū)分線程棧和堆最冰。對于硬件瘦棋,所有的線程棧和堆都分布在主內(nèi)中。部分線程棧和堆可能有時候會出現(xiàn)在CPU緩存中和CPU內(nèi)部的寄存器中暖哨。如下圖所示:
當對象和變量被存放在計算機中各種不同的內(nèi)存區(qū)域中時赌朋,就可能會出現(xiàn)一些具體的問題。主要包括如下兩個方面:
- 線程對共享變量修改的可見性
- 當讀,寫和檢查共享變量時出現(xiàn)race conditions
下面我們專門來解釋以下這兩個問題沛慢。
共享對象可見性
如果兩個或者更多的線程在沒有正確的使用Volatile
聲明或者同步的情況下共享一個對象赡若,一個線程更新這個共享對象可能對其它線程來說是不接見的。
想象一下团甲,共享對象被初始化在主存中逾冬。跑在CPU上的一個線程將這個共享對象讀到CPU緩存中。然后修改了這個對象躺苦。只要CPU緩存沒有被刷新會主存身腻,對象修改后的版本對跑在其它CPU上的線程都是不可見的。這種方式可能導致每個線程擁有這個共享對象的私有拷貝匹厘,每個拷貝停留在不同的CPU緩存中嘀趟。
下圖示意了這種情形。跑在左邊CPU的線程拷貝這個共享對象到它的CPU緩存中集乔,然后將count變量的值修改為2去件。這個修改對跑在右邊CPU上的其它線程是不可見的,因為修改后的count的值還沒有被刷新回主存中去扰路。
解決這個問題你可以使用Java中的volatile關鍵字尤溜。volatile關鍵字可以保證直接從主存中讀取一個變量,如果這個變量被修改后汗唱,總是會被寫回到主存中去宫莱。
Race Conditions
如果兩個或者更多的線程共享一個對象,多個線程在這個共享對象上更新變量哩罪,就有可能發(fā)生race conditions授霸。
想象一下,如果線程A讀一個共享對象的變量count到它的CPU緩存中际插。再想象一下碘耳,線程B也做了同樣的事情,但是往一個不同的CPU緩存中】虺冢現(xiàn)在線程A將count
加1辛辨,線程B也做了同樣的事情。現(xiàn)在count
已經(jīng)被增在了兩個瑟枫,每個CPU緩存中一次斗搞。
如果這些增加操作被順序的執(zhí)行,變量count
應該被增加兩次慷妙,然后原值+2被寫回到主存中去僻焚。
然而,兩次增加都是在沒有適當?shù)耐较虏l(fā)執(zhí)行的膝擂。無論是線程A還是線程B將count
修改后的版本寫回到主存中取虑啤,修改后的值僅會被原值大1隙弛,盡管增加了兩次。
下圖演示了上面描述的情況:
解決這個問題可以使用Java同步塊咐旧。
一個同步塊可以保證在同一時刻僅有一個線程可以進入代碼的臨界區(qū)驶鹉。
同步塊還可以保證代碼塊中所有被訪問的變量將會從主存中讀入,當線程退出同步代碼塊時铣墨,所有被更新的變量都會被刷新回主存中去室埋,不管這個變量是否被聲明為volatile。