Java 內(nèi)存模型(JMM)描述了 JVM 如何使用計(jì)算機(jī)的內(nèi)存(RAM)。JVM 是一個(gè)完整計(jì)算機(jī)的模型惯驼,因此該模型包含了內(nèi)存模型的設(shè)計(jì) —— JMM。
如果要正確地設(shè)計(jì)并發(fā)程序,了解 JMM 非常重要汁雷。JMM 描述了不同線程間如何以及何時(shí)可以看到其它線程寫(xiě)入共享變量的值,以及如何在必要時(shí)同步訪問(wèn)共享變量骇扇。
最初的 JMM 設(shè)計(jì)不充分摔竿,因此 JMM 在 Java 1.5 進(jìn)行了修訂。此版本的 JMM 仍在 Java 8 中使用少孝。
Java Memory Model 內(nèi)部實(shí)現(xiàn)
JVM 內(nèi)部使用的 JMM 將內(nèi)存劃分為線程棧和堆继低。下圖從邏輯角度說(shuō)明了 JMM:
在 JVM 中運(yùn)行的每個(gè)線程都有它自己的線程棧,線程棧包含了線程調(diào)用了哪些方法以到達(dá)當(dāng)前執(zhí)行點(diǎn)的信息稍走,我們把它成為“調(diào)用棧(Call Stack)“袁翁。當(dāng)線程執(zhí)行其代碼時(shí),調(diào)用棧會(huì)發(fā)生變化婿脸。
線程棧還包含了正在執(zhí)行的每個(gè)方法的所有的局部變量(調(diào)用棧上的所有方法)粱胜。一個(gè)線程只能訪問(wèn)它自己的線程棧,由線程創(chuàng)建的局部變量對(duì)于創(chuàng)建它的線程以外的所有其他線程都是不可見(jiàn)的狐树。即使兩個(gè)線程正在執(zhí)行完全相同的代碼焙压,兩個(gè)線程仍將在各自的線程棧中創(chuàng)建自己的局部變量。因此抑钟,每個(gè)線程都有自己的每個(gè)局部變量的版本涯曲。
基本類(lèi)型(boolean,byte在塔,short幻件,char,int蛔溃,long绰沥,float,double)完全存儲(chǔ)在線程棧里贺待,因此對(duì)其他線程是不可見(jiàn)的徽曲。一個(gè)線程可以將一個(gè)基本類(lèi)型的變量副本傳遞給另一個(gè)線程,但它不能共享原始局部變量本身麸塞。
堆包含了 Java 應(yīng)用程序中創(chuàng)建的所有對(duì)象秃臣,不管對(duì)象是哪個(gè)線程創(chuàng)建的,這包括基本類(lèi)型的包裝版本(如 Byte喘垂,Integer甜刻,Long 等)绍撞。無(wú)論對(duì)象是創(chuàng)建成局部變量,還是作為另一個(gè)對(duì)象的成員變量被創(chuàng)建得院,對(duì)象都存儲(chǔ)在堆中傻铣。
下圖說(shuō)明了調(diào)用棧和局部變量存儲(chǔ)在線程棧中,而對(duì)象存儲(chǔ)在堆中祥绞。
局部變量如果是基本類(lèi)型非洲,這種情況下,變量完全存儲(chǔ)在線程棧上蜕径。
局部變量如果是對(duì)象的引用两踏,這種情況下,引用(局部變量)存儲(chǔ)在線程棧上兜喻,但對(duì)象本身存儲(chǔ)在堆上梦染。
對(duì)象中可能包含方法,而這些方法中可能包含局部變量朴皆,這種情況下帕识,即使方法所屬的對(duì)象存儲(chǔ)在堆上,但這些局部變量卻是存儲(chǔ)在線程棧上的遂铡。
對(duì)象的成員變量與對(duì)象本身一起存儲(chǔ)在堆上肮疗,當(dāng)成員變量是基本類(lèi)型以及是對(duì)象的引用時(shí)都是如此。
靜態(tài)類(lèi)型變量與類(lèi)定義一起存儲(chǔ)在堆上扒接。
所有線程通過(guò)擁有對(duì)象引用去訪問(wèn)堆中的對(duì)象伪货。當(dāng)一個(gè)線程有權(quán)訪問(wèn)一個(gè)對(duì)象時(shí),它也能訪問(wèn)該對(duì)象的成員變量钾怔。如果兩個(gè)線程同一時(shí)間調(diào)用同一對(duì)象的一個(gè)方法碱呼,它們都可以訪問(wèn)該對(duì)象的成員變量,但每個(gè)線程都有自己局部變量的副本蒂教。
這是一個(gè)說(shuō)明上述要點(diǎn)的圖表:
兩個(gè)線程各有一組局部變量巍举,其中一個(gè)局部變量(Local Variable 2)指向堆中的共享對(duì)象(Object 3)脆荷。兩個(gè)線程各自對(duì)同一各對(duì)象擁有不同的引用凝垛,它們的引用是局部變量,因此它們存儲(chǔ)在各自線程的線程棧中蜓谋。但是梦皮,這兩個(gè)不同引用指向堆中的同一個(gè)對(duì)象。
請(qǐng)注意桃焕,共享對(duì)象(Object 3)將 Object 2 和 Object 4 作為成員變量引用(如從 Object 3 到 Object 2 和 Object 4 的箭頭所示)剑肯,通過(guò)對(duì)象 3 中的這些成員變量引用,兩個(gè)線程可以訪問(wèn)對(duì)象 2 和 對(duì)象 4观堂。
上圖還顯示了一個(gè)局部變量指向堆中的兩個(gè)不同對(duì)象让网。這種情況下呀忧,引用指向兩個(gè)不同的對(duì)象(Object 1 和 Object 5),而不是同一個(gè)對(duì)象溃睹。理論上而账,如果兩個(gè)線程都引用了兩個(gè)對(duì)象,那兩個(gè)線程都可以訪問(wèn)對(duì)象 1 和 對(duì)象 5因篇。但在上圖中泞辐,每個(gè)線程只引用了兩個(gè)對(duì)象中的一個(gè)。
那么竞滓,什么樣的 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;
//... 使用局部變量做更多事情.
methodTwo();
}
public void methodTwo() {
Integer localVariable1 = new Integer(99);
//... 使用局部變量做更多事情.
}
}
public class MySharedObject {
// 指向MySharedObject實(shí)例的靜態(tài)變量
public static final MySharedObject sharedInstance =
new MySharedObject();
// 成員變量指向堆上的兩個(gè)對(duì)象
public Integer object2 = new Integer(22);
public Integer object4 = new Integer(44);
public long member1 = 12345;
public long member1 = 67890;
}
如果兩個(gè)線程正在執(zhí)行 run() 方法商佑,則前面的結(jié)果就會(huì)出現(xiàn)锯茄。run() 方法會(huì)調(diào)用 methodOne(),而 methodOne() 會(huì)調(diào)用 methodTwo()茶没。
方法 methodOne() 中聲明了一個(gè)基本類(lèi)型的局部變量(localVariable1 類(lèi)型 int)和一個(gè)對(duì)象引用的局部變量(localVariable2)撇吞。
每個(gè)執(zhí)行 methodOne() 的線程將在各自的線程棧上創(chuàng)建自己的 localVariable1 和 localVariable2 副本。localVariable 1 變量將完全分離礁叔,只存在于每個(gè)線程的線程棧中牍颈。一個(gè)線程無(wú)法看到另一個(gè)線程對(duì)其 localVariable 1 副本所做的更改。
執(zhí)行 methodOne() 的每個(gè)線程還將創(chuàng)建它們自己的 localVariable2 副本琅关。然而煮岁,localVariable 2 的兩個(gè)不同副本最終都指向堆上的同一個(gè)對(duì)象。代碼將 localVariable 2 設(shè)置為指向靜態(tài)變量引用的對(duì)象涣易。靜態(tài)變量只有一個(gè)副本画机,這個(gè)副本存儲(chǔ)在堆上。因此新症,localVariable 2 的兩個(gè)副本最終都指向靜態(tài)變量所指向的 MySharedObject 的同一個(gè)實(shí)例步氏。MySharedObject 實(shí)例也存儲(chǔ)在堆中,它對(duì)應(yīng)于上圖中的對(duì)象 3徒爹。
注意 MySharedObject 類(lèi)也包含兩個(gè)成員變量荚醒。成員變量本身同對(duì)象一起存儲(chǔ)在堆中。這兩個(gè)成員變量指向另外兩個(gè) Integer 對(duì)象隆嗅,這些 Integer 對(duì)象對(duì)應(yīng)于上圖中的對(duì)象 2和對(duì)象 4界阁。
還要注意 methodTwo() 創(chuàng)建的一個(gè)名為 localVariable 1 的本地變量。這個(gè)局部變量是一個(gè)指向 Integer 對(duì)象的對(duì)象引用胖喳。該方法將 localVariable 1 引用設(shè)置為指向一個(gè)新的 Integer 實(shí)例泡躯。localVariable 1 引用將存儲(chǔ)在每個(gè)執(zhí)行 methodTwo() 的線程的一個(gè)副本中。實(shí)例化的兩個(gè) Integer 對(duì)象存儲(chǔ)在堆上,但是由于方法每次執(zhí)行都會(huì)創(chuàng)建一個(gè)新的 Integer 對(duì)象较剃,因此執(zhí)行該方法的兩個(gè)線程將創(chuàng)建單獨(dú)的 Integer 實(shí)例咕别。methodTwo() 中創(chuàng)建的 Integer 對(duì)象對(duì)應(yīng)于上圖中的對(duì)象 1和對(duì)象 5。還要注意類(lèi) MySharedObject 中的兩個(gè)成員變量写穴,它們的類(lèi)型是 long顷级,這是一個(gè)基本類(lèi)型。由于這些變量是成員變量确垫,所以它們?nèi)匀慌c對(duì)象一起存儲(chǔ)在堆中弓颈。只有本地變量存儲(chǔ)在線程堆棧中。
硬件內(nèi)存架構(gòu)
現(xiàn)代硬件內(nèi)存架構(gòu)與 Java 內(nèi)存模型略有不同删掀。了解硬件內(nèi)存架構(gòu)也很重要翔冀,以了解 Java 內(nèi)存模型如何與其一起工作。本節(jié)介紹了常見(jiàn)的硬件內(nèi)存架構(gòu)披泪,后面的部分將介紹 Java 內(nèi)存模型如何與其配合使用纤子。
這是現(xiàn)代計(jì)算機(jī)硬件架構(gòu)的簡(jiǎn)化圖:
現(xiàn)代計(jì)算機(jī)通常有兩個(gè)或更多的 CPU,其中一些 CPU 也可能有多個(gè)內(nèi)核款票。關(guān)鍵是控硼,在具有2個(gè)或更多 CPU 的現(xiàn)代計(jì)算機(jī)上,可以同時(shí)運(yùn)行多個(gè)線程艾少。每個(gè) CPU 都能夠在任何給定時(shí)間運(yùn)行一個(gè)線程卡乾。這意味著如果您的 Java 應(yīng)用程序是多線程的,那么每個(gè) CPU 可能同時(shí)(并發(fā)地)運(yùn)行 Java 應(yīng)用程序中的一個(gè)線程缚够。
每個(gè) CPU 包含一組寄存器幔妨,這些寄存器本質(zhì)上是在 CPU 內(nèi)存中。CPU 在這些寄存器上執(zhí)行操作的速度要比在主內(nèi)存中執(zhí)行變量的速度快得多谍椅。這是因?yàn)?CPU 訪問(wèn)這些寄存器的速度要比訪問(wèn)主內(nèi)存快得多误堡。
每個(gè) CPU 還可以有一個(gè) CPU 緩存內(nèi)存層。事實(shí)上雏吭,大多數(shù)現(xiàn)代 CPU 都有某種大小的緩存內(nèi)存層锁施。CPU 訪問(wèn)緩存內(nèi)存的速度比主內(nèi)存快得多,但通常沒(méi)有訪問(wèn)內(nèi)部寄存器的速度快杖们。因此悉抵,CPU 高速緩存存儲(chǔ)器介于內(nèi)部寄存器和主存儲(chǔ)器的速度之間。某些 CPU 可能有多個(gè)緩存層(L1 和 L2)胀莹,但要了解 Java 內(nèi)存模型如何與內(nèi)存交互基跑,這一點(diǎn)并不重要婚温。重要的是要知道 CPU 可以有某種緩存存儲(chǔ)層描焰。
計(jì)算機(jī)還包含一個(gè)主內(nèi)存區(qū)域(RAM)。所有 CPU 都可以訪問(wèn)主存,主內(nèi)存區(qū)域通常比 CPU 的緩存內(nèi)存大得多荆秦。
通常篱竭,當(dāng) CPU 需要訪問(wèn)主內(nèi)存時(shí),它會(huì)將部分主內(nèi)存讀入 CPU 緩存步绸。它甚至可以將緩存的一部分讀入內(nèi)部寄存器掺逼,然后對(duì)其執(zhí)行操作。當(dāng) CPU 需要將結(jié)果寫(xiě)回主內(nèi)存時(shí)瓤介,它會(huì)將值從內(nèi)部寄存器刷新到緩存內(nèi)存吕喘,并在某個(gè)時(shí)候?qū)⒅邓⑿禄刂鲀?nèi)存。
當(dāng)CPU需要在高速緩存中存儲(chǔ)其他內(nèi)容時(shí)刑桑,通常會(huì)將存儲(chǔ)在高速緩存中的值刷新回主內(nèi)存氯质。CPU 緩存可以一次將數(shù)據(jù)寫(xiě)入一部分內(nèi)存,并一次刷新一部分內(nèi)存祠斧。它不必每次更新時(shí)都讀取/寫(xiě)入完整的緩存闻察。通常,緩存是在稱(chēng)為“緩存線(Cache Line)”的較小內(nèi)存塊中更新的琢锋≡可以將一條或多條高速緩存線讀入高速緩存內(nèi)存,并將一條或多條高速緩存線再次刷新回主內(nèi)存吴超。
JMM 和硬件內(nèi)存結(jié)構(gòu)之間的差別
如前所述钉嘹,JMM 和硬件內(nèi)存結(jié)構(gòu)是不同的。硬件內(nèi)存體系結(jié)構(gòu)不區(qū)分線程棧和堆鲸阻。在硬件上隧期,線程棧和堆都位于主內(nèi)存中。線程棧和堆的一部分有時(shí)可能存在于 CPU 高速緩存和內(nèi)部 CPU 寄存器中赘娄。如下圖所示:
當(dāng)對(duì)象和變量可以存儲(chǔ)在計(jì)算機(jī)的不同內(nèi)存區(qū)域時(shí)仆潮,可能會(huì)出現(xiàn)某些問(wèn)題。主要有兩個(gè)問(wèn)題:
線程更新(寫(xiě)入)對(duì)共享變量的可見(jiàn)性
讀取遣臼、檢查和寫(xiě)入共享變量時(shí)的競(jìng)爭(zhēng)條件
這兩個(gè)問(wèn)題將在下面幾節(jié)中進(jìn)行解釋性置。
共享對(duì)象的可見(jiàn)性
如果兩個(gè)或多個(gè)線程共享一個(gè)對(duì)象,而沒(méi)有正確使用 volatile 聲明或同步揍堰,那么一個(gè)線程對(duì)共享對(duì)象的更新可能對(duì)其他線程不可見(jiàn)鹏浅。
假設(shè)共享對(duì)象最初存儲(chǔ)在主內(nèi)存中。在 CPU 1 上運(yùn)行的線程然后將共享對(duì)象讀入它的 CPU 緩存屏歹。在這里隐砸,它對(duì)共享對(duì)象進(jìn)行更改。只要沒(méi)有將 CPU 緩存刷新回主內(nèi)存蝙眶,在其他 CPU 上運(yùn)行的線程就不會(huì)看到共享對(duì)象的更改版本季希。這樣褪那,每個(gè)線程都可能最終擁有自己的共享對(duì)象副本,每個(gè)副本位于不同的 CPU緩 存中式塌。
下圖說(shuō)明了大致的情況博敬。在左 CPU 上運(yùn)行的一個(gè)線程將共享對(duì)象復(fù)制到其 CPU 緩存中,并將其 count 變量更改為2峰尝。此更改對(duì)運(yùn)行在正確 CPU 上的其他線程不可見(jiàn)偏窝,因?yàn)樯形磳⒏滤⑿禄刂鲀?nèi)存。
要解決這個(gè)問(wèn)題,可以使用 Java 的 volatile 關(guān)鍵字。volatile 關(guān)鍵字可以確保直接從主內(nèi)存讀取給定的變量荡含,并在更新時(shí)始終將其寫(xiě)回主內(nèi)存。
競(jìng)態(tài)條件
如果兩個(gè)或多個(gè)線程共享一個(gè)對(duì)象链沼,且多個(gè)線程更新該共享對(duì)象中的變量,則可能出現(xiàn)競(jìng)爭(zhēng)條件沛鸵。
假設(shè)線程 A 將共享對(duì)象的變量計(jì)數(shù)讀入其 CPU 緩存括勺。再想象一下,線程 B 執(zhí)行相同的操作曲掰,但是進(jìn)入了不同的 CPU 緩存〖埠矗現(xiàn)在線程 A 向 count 加一,線程 B 也這樣做±秆現(xiàn)在 var1 已經(jīng)增加了兩次乱豆,每次在每個(gè) CPU 緩存中增加一次。
如果按順序執(zhí)行這些增量吊趾,變量計(jì)數(shù)將增加兩次宛裕,并將原始值 + 2 寫(xiě)回主內(nèi)存。
但是论泛,這兩個(gè)增量是同時(shí)執(zhí)行的揩尸,沒(méi)有適當(dāng)?shù)耐健o(wú)論哪個(gè)線程 A 和線程 B 將其更新版本的 count 寫(xiě)回主內(nèi)存屁奏,更新后的值只比原始值高1岩榆,盡管有兩個(gè)增量。
該圖說(shuō)明了上述競(jìng)態(tài)條件問(wèn)題的發(fā)生情況:
要解決這個(gè)問(wèn)題坟瓢,可以使用 Java synchronized 塊勇边。同步塊保證在任何給定時(shí)間只有一個(gè)線程可以進(jìn)入代碼的給定臨界段。Synchronized 塊還保證在 Synchronized 塊中訪問(wèn)的所有變量都將從主內(nèi)存中讀入折联,當(dāng)線程退出 Synchronized 塊時(shí)粒褒,所有更新的變量將再次刷新回主內(nèi)存,而不管變量是否聲明為 volatile诚镰。
在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流群奕坟。交流學(xué)習(xí)群號(hào):833145934 里面資深架構(gòu)師會(huì)分享一些整理好的錄制視頻錄像和BATJ面試題:有Spring祥款,MyBatis,Netty源碼分析执赡,高并發(fā)镰踏、高性能函筋、分布式沙合、微服務(wù)架構(gòu)的原理,JVM性能優(yōu)化跌帐、分布式架構(gòu)等這些成為架構(gòu)師必備的知識(shí)體系首懈。還能領(lǐng)取免費(fèi)的學(xué)習(xí)資源,目前受益良多谨敛。
注:本文轉(zhuǎn)載自 linkedkeeper.com (文/張松然)