知識(shí)
不管是網(wǎng)站,軟件還是小程序,都要直接或間接能為您產(chǎn)生價(jià)值,我們?cè)谧非笃湟曈X(jué)表現(xiàn)的同時(shí),更側(cè)重于功能的便捷,營(yíng)銷的便利,運(yùn)營(yíng)的高效,讓網(wǎng)站成為營(yíng)銷工具,讓軟件能切實(shí)提升企業(yè)內(nèi)部管理水平和效率。優(yōu)秀的程序?yàn)楹笃谏?jí)提供便捷的支持!
阿里P8架構(gòu)師詳談 Java 內(nèi)存模型
發(fā)表時(shí)間:2019-7-5
發(fā)布人:葵宇科技
瀏覽次數(shù):48
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è)局部變量的版本。
基本類型(boolean,byte,short,char,int,long,float,double)完全存儲(chǔ)在線程棧里,因此對(duì)其他線程是不可見(jiàn)的。一個(gè)線程可以將一個(gè)基本類型的變量副本傳遞給另一個(gè)線程,但它不能共享原始局部變量本身。
堆包含了 Java 應(yīng)用程序中創(chuàng)建的所有對(duì)象,不管對(duì)象是哪個(gè)線程創(chuàng)建的,這包括基本類型的包裝版本(如 Byte,Integer,Long 等)。無(wú)論對(duì)象是創(chuàng)建成局部變量,還是作為另一個(gè)對(duì)象的成員變量被創(chuàng)建,對(duì)象都存儲(chǔ)在堆中。
下圖說(shuō)明了調(diào)用棧和局部變量存儲(chǔ)在線程棧中,而對(duì)象存儲(chǔ)在堆中。
局部變量如果是基本類型,這種情況下,變量完全存儲(chǔ)在線程棧上。
局部變量如果是對(duì)象的引用,這種情況下,引用(局部變量)存儲(chǔ)在線程棧上,但對(duì)象本身存儲(chǔ)在堆上。
對(duì)象中可能包含方法,而這些方法中可能包含局部變量,這種情況下,即使方法所屬的對(duì)象存儲(chǔ)在堆上,但這些局部變量卻是存儲(chǔ)在線程棧上的。
對(duì)象的成員變量與對(duì)象本身一起存儲(chǔ)在堆上,當(dāng)成員變量是基本類型以及是對(duì)象的引用時(shí)都是如此。
靜態(tà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學(xué)習(xí)圈子
那么,什么樣的 Java 代碼可以導(dǎo)致上面的內(nèi)存圖?好吧,代碼就如下面的代碼一樣簡(jiǎn)單:
如果兩個(gè)線程正在執(zhí)行 run() 方法,則前面的結(jié)果就會(huì)出現(xiàn)。run() 方法會(huì)調(diào)用 methodOne(),而 methodOne() 會(huì)調(diào)用 methodTwo()。
方法 methodOne() 中聲明了一個(gè)基本類型的局部變量(localVariable1 類型 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。Java學(xué)習(xí)圈子???????
注意 MySharedObject 類也包含兩個(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。還要注意類 MySharedObject 中的兩個(gè)成員變量,它們的類型是 long,這是一個(gè)基本類型。由于這些變量是成員變量,所以它們?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ě)入完整的緩存。通常,緩存是在稱為“緩存線(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)行解釋。Java學(xué)習(xí)圈子???????
共享對(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 緩存?,F(xiàn)在線程 A 向 count 加一,線程 B 也這樣做?,F(xiàn)在 var1 已經(jīng)增加了兩次,每次在每個(gè) CPU 緩存中增加一次。
如果按順序執(zhí)行這些增量,變量計(jì)數(shù)將增加兩次,并將原始值 + 2 寫(xiě)回主內(nèi)存。
但是,這兩個(gè)增量是同時(shí)執(zhí)行的,沒(méi)有適當(dāng)?shù)耐?。無(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。
粉絲福利:
為粉絲講解福利資源:特講解免費(fèi)教程教你如何學(xué)習(xí) ,源碼、分布式、微服務(wù)、性能優(yōu)化、多線程并發(fā),從0到1,帶你領(lǐng)略底層精髓。
如何學(xué)習(xí):
上圖中的資料都是我精心錄制視頻,感興趣的可以加入我的Java學(xué)習(xí)圈子 免費(fèi)獲取。希望能夠在你接下來(lái)即將應(yīng)對(duì)的的面試過(guò)程中能夠盡到一份綿薄之力。
相關(guān)案例查看更多
相關(guān)閱讀
- 小程序開(kāi)發(fā)
- 云南小程序開(kāi)發(fā)哪家好
- 網(wǎng)站制作
- web學(xué)習(xí)路線
- 開(kāi)發(fā)制作小程序
- 云南網(wǎng)站建設(shè)快速優(yōu)化
- 重慶網(wǎng)站建設(shè)公司
- 云南etc小程序
- 云南網(wǎng)絡(luò)營(yíng)銷顧問(wèn)
- 前端開(kāi)發(fā)
- 汽車報(bào)廢管理
- 開(kāi)通微信小程序被騙
- 搜索排名
- web開(kāi)發(fā)
- 昆明網(wǎng)絡(luò)公司
- 網(wǎng)絡(luò)營(yíng)銷
- 生成海報(bào)
- 網(wǎng)站上首頁(yè)
- 昆明網(wǎng)站建設(shè)公司
- 云南網(wǎng)站建設(shè)優(yōu)化
- 云南網(wǎng)站建設(shè)費(fèi)用
- 云南網(wǎng)站建設(shè)電話
- 云南做網(wǎng)站
- 報(bào)廢車
- 小程序開(kāi)發(fā)公司
- 大理小程序開(kāi)發(fā)
- 網(wǎng)站建設(shè)費(fèi)用
- 云南軟件公司
- 電商網(wǎng)站建設(shè)
- 海南小程序制作公司