阿裡P8架構師(shī)詳談 Java 内存模型
發表時間:2019-7-5
發布人:葵宇科技
浏覽次數:28
Java 内存模型(JMM)描述了 JVM 如(rú)何使用計算機的内存(RAM)。JVM 是一個(gè)完整計算機的模型,因此該模型包含了内存模型的設計 —— JMM。
如(rú)果要正确地設計并發程序,了解 JMM 非常重要。JMM 描述了不同線程間如(rú)何以及何時可(kě)以看到其它線程寫入共享變量的值,以及如(rú)何在必要時同步訪問(wèn)共享變量。
最初的 JMM 設計不充分,因此 JMM 在 Java 1.5 進行了修訂。此版本的 JMM 仍在 Java 8 中(zhōng)使用。
Java Memory Model 内部實現
JVM 内部使用的 JMM 将内存劃分為線程棧和(hé)堆。下(xià)圖從邏輯角度說明了 JMM:
在 JVM 中(zhōng)運行的每個(gè)線程都有它自己的線程棧,線程棧包含了線程調用了哪些方法以到達當前執行點的信息,我們把它成為“調用棧(Call Stack)“。當線程執行其代碼時,調用棧會發生變化。
線程棧還包含了正在執行的每個(gè)方法的所有的局部變量(調用棧上的所有方法)。一個(gè)線程隻能訪問(wèn)它自己的線程棧,由線程創建的局部變量對于創建它的線程以外的所有其他線程都是不可(kě)見的。即使兩個(gè)線程正在執行完全相同的代碼,兩個(gè)線程仍将在各自的線程棧中(zhōng)創建自己的局部變量。因此,每個(gè)線程都有自己的每個(gè)局部變量的版本。
基本類型(boolean,byte,short,char,int,long,float,double)完全存儲在線程棧裡,因此對其他線程是不可(kě)見的。一個(gè)線程可(kě)以将一個(gè)基本類型的變量副本傳遞給另一個(gè)線程,但它不能共享原始局部變量本身。
堆包含了 Java 應用程序中(zhōng)創建的所有對象,不管對象是哪個(gè)線程創建的,這包括基本類型的包裝版本(如(rú) Byte,Integer,Long 等)。無論對象是創建成局部變量,還是作為另一個(gè)對象的成員變量被創建,對象都存儲在堆中(zhōng)。
下(xià)圖說明了調用棧和(hé)局部變量存儲在線程棧中(zhōng),而對象存儲在堆中(zhōng)。
局部變量如(rú)果是基本類型,這種情況下(xià),變量完全存儲在線程棧上。
局部變量如(rú)果是對象的引用,這種情況下(xià),引用(局部變量)存儲在線程棧上,但對象本身存儲在堆上。
對象中(zhōng)可(kě)能包含方法,而這些方法中(zhōng)可(kě)能包含局部變量,這種情況下(xià),即使方法所屬的對象存儲在堆上,但這些局部變量卻是存儲在線程棧上的。
對象的成員變量與對象本身一起存儲在堆上,當成員變量是基本類型以及是對象的引用時都是如(rú)此。
靜态類型變量與類定義一起存儲在堆上。
所有線程通(tōng)過擁有對象引用去訪問(wèn)堆中(zhōng)的對象。當一個(gè)線程有權訪問(wèn)一個(gè)對象時,它也能訪問(wèn)該對象的成員變量。如(rú)果兩個(gè)線程同一時間調用同一對象的一個(gè)方法,它們都可(kě)以訪問(wèn)該對象的成員變量,但每個(gè)線程都有自己局部變量的副本。
這是一個(gè)說明上述要點的圖表:
兩個(gè)線程各有一組局部變量,其中(zhōng)一個(gè)局部變量(Local Variable 2)指向堆中(zhōng)的共享對象(Object 3)。兩個(gè)線程各自對同一各對象擁有不同的引用,它們的引用是局部變量,因此它們存儲在各自線程的線程棧中(zhōng)。但是,這兩個(gè)不同引用指向堆中(zhōng)的同一個(gè)對象。
請注意,共享對象(Object 3)将 Object 2 和(hé) Object 4 作為成員變量引用(如(rú)從 Object 3 到 Object 2 和(hé) Object 4 的箭頭所示),通(tōng)過對象 3 中(zhōng)的這些成員變量引用,兩個(gè)線程可(kě)以訪問(wèn)對象 2 和(hé) 對象 4。
上圖還顯示了一個(gè)局部變量指向堆中(zhōng)的兩個(gè)不同對象。這種情況下(xià),引用指向兩個(gè)不同的對象(Object 1 和(hé) Object 5),而不是同一個(gè)對象。理論上,如(rú)果兩個(gè)線程都引用了兩個(gè)對象,那兩個(gè)線程都可(kě)以訪問(wèn)對象 1 和(hé) 對象 5。但在上圖中(zhōng),每個(gè)線程隻引用了兩個(gè)對象中(zhōng)的一個(gè)。Java學習圈子(zǐ)
那麼,什麼樣的 Java 代碼可(kě)以導緻上面的内存圖?好吧,代碼就如(rú)下(xià)面的代碼一樣簡單:
如(rú)果兩個(gè)線程正在執行 run() 方法,則前面的結果就會出現。run() 方法會調用 methodOne(),而 methodOne() 會調用 methodTwo()。
方法 methodOne() 中(zhōng)聲明了一個(gè)基本類型的局部變量(localVariable1 類型 int)和(hé)一個(gè)對象引用的局部變量(localVariable2)。
每個(gè)執行 methodOne() 的線程将在各自的線程棧上創建自己的 localVariable1 和(hé) localVariable2 副本。localVariable 1 變量将完全分離(lí),隻存在于每個(gè)線程的線程棧中(zhōng)。一個(gè)線程無法看到另一個(gè)線程對其 localVariable 1 副本所做的更改。
執行 methodOne() 的每個(gè)線程還将創建它們自己的 localVariable2 副本。然而,localVariable 2 的兩個(gè)不同副本最終都指向堆上的同一個(gè)對象。代碼将 localVariable 2 設置為指向靜态變量引用的對象。靜态變量隻有一個(gè)副本,這個(gè)副本存儲在堆上。因此,localVariable 2 的兩個(gè)副本最終都指向靜态變量所指向的 MySharedObject 的同一個(gè)實例。MySharedObject 實例也存儲在堆中(zhōng),它對應于上圖中(zhōng)的對象 3。Java學習圈子(zǐ)???????
注意 MySharedObject 類也包含兩個(gè)成員變量。成員變量本身同對象一起存儲在堆中(zhōng)。這兩個(gè)成員變量指向另外兩個(gè) Integer 對象,這些 Integer 對象對應于上圖中(zhōng)的對象 2和(hé)對象 4。
還要注意 methodTwo() 創建的一個(gè)名為 localVariable 1 的本地變量。這個(gè)局部變量是一個(gè)指向 Integer 對象的對象引用。該方法将 localVariable 1 引用設置為指向一個(gè)新的 Integer 實例。localVariable 1 引用将存儲在每個(gè)執行 methodTwo() 的線程的一個(gè)副本中(zhōng)。實例化的兩個(gè) Integer 對象存儲在堆上,但是由于方法每次執行都會創建一個(gè)新的 Integer 對象,因此執行該方法的兩個(gè)線程将創建單獨的 Integer 實例。methodTwo() 中(zhōng)創建的 Integer 對象對應于上圖中(zhōng)的對象 1和(hé)對象 5。還要注意類 MySharedObject 中(zhōng)的兩個(gè)成員變量,它們的類型是 long,這是一個(gè)基本類型。由于這些變量是成員變量,所以它們仍然與對象一起存儲在堆中(zhōng)。隻有本地變量存儲在線程堆棧中(zhōng)。
硬件内存架構
現代硬件内存架構與 Java 内存模型略有不同。了解硬件内存架構也很重要,以了解 Java 内存模型如(rú)何與其一起工作。本節介紹了常見的硬件内存架構,後面的部分将介紹 Java 内存模型如(rú)何與其配合使用。
這是現代計算機硬件架構的簡化圖:
現代計算機通(tōng)常有兩個(gè)或更多的 CPU,其中(zhōng)一些 CPU 也可(kě)能有多個(gè)内核。關(guān)鍵是,在具有2個(gè)或更多 CPU 的現代計算機上,可(kě)以同時運行多個(gè)線程。每個(gè) CPU 都能夠在任何給定時間運行一個(gè)線程。這意味着如(rú)果您的 Java 應用程序是多線程的,那麼每個(gè) CPU 可(kě)能同時(并發地)運行 Java 應用程序中(zhōng)的一個(gè)線程。
每個(gè) CPU 包含一組寄存器(qì),這些寄存器(qì)本質上是在 CPU 内存中(zhōng)。CPU 在這些寄存器(qì)上執行操作的速度要比在主内存中(zhōng)執行變量的速度快得多。這是因為 CPU 訪問(wèn)這些寄存器(qì)的速度要比訪問(wèn)主内存快得多。
每個(gè) CPU 還可(kě)以有一個(gè) CPU 緩存内存層。事實上,大多數現代 CPU 都有某種大小的緩存内存層。CPU 訪問(wèn)緩存内存的速度比主内存快得多,但通(tōng)常沒有訪問(wèn)内部寄存器(qì)的速度快。因此,CPU 高速緩存存儲器(qì)介于内部寄存器(qì)和(hé)主存儲器(qì)的速度之間。某些 CPU 可(kě)能有多個(gè)緩存層(L1 和(hé) L2),但要了解 Java 内存模型如(rú)何與内存交互,這一點并不重要。重要的是要知道 CPU 可(kě)以有某種緩存存儲層。
計算機還包含一個(gè)主内存區域(RAM)。所有 CPU 都可(kě)以訪問(wèn)主存,主内存區域通(tōng)常比 CPU 的緩存内存大得多。
通(tōng)常,當 CPU 需要訪問(wèn)主内存時,它會将部分主内存讀入 CPU 緩存。它甚至可(kě)以将緩存的一部分讀入内部寄存器(qì),然後對其執行操作。當 CPU 需要将結果寫回主内存時,它會将值從内部寄存器(qì)刷新到緩存内存,并在某個(gè)時候将值刷新回主内存。
當CPU需要在高速緩存中(zhōng)存儲其他内容時,通(tōng)常會将存儲在高速緩存中(zhōng)的值刷新回主内存。CPU 緩存可(kě)以一次将數據寫入一部分内存,并一次刷新一部分内存。它不必每次更新時都讀取/寫入完整的緩存。通(tōng)常,緩存是在稱為“緩存線(Cache Line)”的較小内存塊中(zhōng)更新的。可(kě)以将一條或多條高速緩存線讀入高速緩存内存,并将一條或多條高速緩存線再次刷新回主内存。
JMM 和(hé)硬件内存結構之間的差别
如(rú)前所述,JMM 和(hé)硬件内存結構是不同的。硬件内存體系結構不區分線程棧和(hé)堆。在硬件上,線程棧和(hé)堆都位于主内存中(zhōng)。線程棧和(hé)堆的一部分有時可(kě)能存在于 CPU 高速緩存和(hé)内部 CPU 寄存器(qì)中(zhōng)。如(rú)下(xià)圖所示:
當對象和(hé)變量可(kě)以存儲在計算機的不同内存區域時,可(kě)能會出現某些問(wèn)題。主要有兩個(gè)問(wèn)題:
- 線程更新(寫入)對共享變量的可(kě)見性
- 讀取、檢查和(hé)寫入共享變量時的競争條件
這兩個(gè)問(wèn)題将在下(xià)面幾節中(zhōng)進行解釋。Java學習圈子(zǐ)???????
共享對象的可(kě)見性
如(rú)果兩個(gè)或多個(gè)線程共享一個(gè)對象,而沒有正确使用 volatile 聲明或同步,那麼一個(gè)線程對共享對象的更新可(kě)能對其他線程不可(kě)見。
假設共享對象最初存儲在主内存中(zhōng)。在 CPU 1 上運行的線程然後将共享對象讀入它的 CPU 緩存。在這裡,它對共享對象進行更改。隻要沒有将 CPU 緩存刷新回主内存,在其他 CPU 上運行的線程就不會看到共享對象的更改版本。這樣,每個(gè)線程都可(kě)能最終擁有自己的共享對象副本,每個(gè)副本位于不同的 CPU緩 存中(zhōng)。
下(xià)圖說明了大緻的情況。在左 CPU 上運行的一個(gè)線程将共享對象複制到其 CPU 緩存中(zhōng),并将其 count 變量更改為2。此更改對運行在正确 CPU 上的其他線程不可(kě)見,因為尚未将更新刷新回主内存。
要解決這個(gè)問(wèn)題,可(kě)以使用 Java 的 volatile 關(guān)鍵字。volatile 關(guān)鍵字可(kě)以确保直接從主内存讀取給定的變量,并在更新時始終将其寫回主内存。
競态條件
如(rú)果兩個(gè)或多個(gè)線程共享一個(gè)對象,且多個(gè)線程更新該共享對象中(zhōng)的變量,則可(kě)能出現競争條件。
假設線程 A 将共享對象的變量計數讀入其 CPU 緩存。再想象一下(xià),線程 B 執行相同的操作,但是進入了不同的 CPU 緩存。現在線程 A 向 count 加一,線程 B 也這樣做。現在 var1 已經增加了兩次,每次在每個(gè) CPU 緩存中(zhōng)增加一次。
如(rú)果按順序執行這些增量,變量計數将增加兩次,并将原始值 + 2 寫回主内存。
但是,這兩個(gè)增量是同時執行的,沒有适當的同步。無論哪個(gè)線程 A 和(hé)線程 B 将其更新版本的 count 寫回主内存,更新後的值隻比原始值高1,盡管有兩個(gè)增量。
該圖說明了上述競态條件問(wèn)題的發生情況:
要解決這個(gè)問(wèn)題,可(kě)以使用 Java synchronized 塊。同步塊保證在任何給定時間隻有一個(gè)線程可(kě)以進入代碼的給定臨界段。Synchronized 塊還保證在 Synchronized 塊中(zhōng)訪問(wèn)的所有變量都将從主内存中(zhōng)讀入,當線程退出 Synchronized 塊時,所有更新的變量将再次刷新回主内存,而不管變量是否聲明為 volatile。
粉絲福利:
為粉絲講解福利資(zī)源:特講解免費教程教你(nǐ)如(rú)何學習 ,源碼、分布式、微服務、性能優化、多線程并發,從0到1,帶你(nǐ)領略底層精髓。
如(rú)何學習:
上圖中(zhōng)的資(zī)料都是我精心錄制視頻,感興趣的可(kě)以加入我的Java學習圈子(zǐ) 免費獲取。希望能夠在你(nǐ)接下(xià)來即将應對的的面試過程中(zhōng)能夠盡到一份綿薄之力。