面試官:什么是Java內(nèi)存模型?
當問到 Java 內(nèi)存模型的時候,一定要注意,Java 內(nèi)存模型(Java Memory Model,JMM)它和 JVM 內(nèi)存布局(JVM 運行時數(shù)據(jù)區(qū)域)是不一樣的,它們是兩個完全不同的概念。
1.為什么要有 Java 內(nèi)存模型?
Java 內(nèi)存模型存在的原因在于解決多線程環(huán)境下并發(fā)執(zhí)行時的內(nèi)存可見性和一致性問題。在現(xiàn)代計算機系統(tǒng)中,尤其是多處理器架構(gòu)下,每個處理器都有自己的高速緩存,而主內(nèi)存(RAM)是所有處理器共享的數(shù)據(jù)存儲區(qū)域。當多個線程同時訪問和修改同一塊共享數(shù)據(jù)時,如果沒有適當?shù)耐綑C制,就可能導致以下問題:
- 可見性:一個線程對共享變量所做的修改可能不會立即反映到另一個線程的視角中,因為這些修改可能只存在于本地緩存中,并未刷新回主內(nèi)存。
- 有序性:編譯器和處理器為了優(yōu)化性能,可能會對指令進行重排序,這可能導致程序在單線程環(huán)境中看似按照源代碼順序執(zhí)行,但在多線程環(huán)境中的實際執(zhí)行順序卻與預期不同。
- 原子性:即使是最簡單的讀取或賦值操作,在硬件層面也不一定保證是原子性的,即在沒有同步的情況下,多線程下可能看到操作只執(zhí)行了一部分的結(jié)果。
Java 內(nèi)存模型通過定義一套規(guī)則來規(guī)范并限制編譯器、運行時以及處理器對內(nèi)存訪問的重排序行為,確保了多線程間的交互具有明確的語義。它規(guī)定了共享變量的訪問規(guī)則、提供了 happens-before 原則以及 volatile 關(guān)鍵字、synchronized 等工具來實現(xiàn)內(nèi)存可見性和一致性的保障。這樣,程序員在編寫并發(fā)代碼時,可以依據(jù)這些規(guī)則來確保代碼的正確執(zhí)行,從而避免由于多線程帶來的不確定性和錯誤。
如果沒有 Java 內(nèi)存模型就會出現(xiàn)以下兩大問題:
- CPU 和 內(nèi)存一致性問題。
- 指令重排序問題。
具體內(nèi)容如下。
(1)一致性問題
要講明白緩存一致性問題,要從計算機的內(nèi)存結(jié)構(gòu)說起,它的結(jié)構(gòu)是這樣的:
所以從上面可以看出計算機的重要組成部分包含以下內(nèi)容:
- CPU
- CPU 寄存器:也叫 L1 緩存,一級緩存。
- CPU 高速緩存:也叫 L2 緩存,二級緩存。
- (主)內(nèi)存
當然,部分高端機器還有 L3 三級緩存。
由于主內(nèi)存與 CPU 處理器的運算能力之間有數(shù)量級的差距,所以在傳統(tǒng)計算機內(nèi)存架構(gòu)中會引入高速緩存(L2)來作為主存和處理器之間的緩沖,CPU 將常用的數(shù)據(jù)放在高速緩存中,運算結(jié)束后 CPU 再講運算結(jié)果同步到主內(nèi)存中,這樣就會導致多個線程在進行操作和同步時,導致 CPU 緩存和主內(nèi)存數(shù)據(jù)不一致的問題。
(2)重排序問題
由于有 JIT(Just In Time,即時編譯)技術(shù)的存在,它可能會對代碼進行優(yōu)化,比如將原本執(zhí)行順序為 a -> b -> c 的流程,“優(yōu)化”成 a -> c -> b 了,但這樣優(yōu)化之后,可能會導致我們的程序在某些場景執(zhí)行出錯,比如單例模式雙重效驗鎖的場景,這就是典型的好心辦壞事的事例。
2.定義
Java 內(nèi)存模型(Java Memory Model,簡稱 JMM)是一種規(guī)范,它定義了 Java 虛擬機(JVM)在計算機內(nèi)存(RAM)中的工作方式,即規(guī)范了 Java 虛擬機與計算機內(nèi)存之間是如何協(xié)同工作的。具體來說,它規(guī)定了一個線程如何和何時可以看到其他線程修改過的共享變量的值,以及在必須時如何同步地訪問共享變量。
3.規(guī)范內(nèi)容
Java 內(nèi)存模型主要包括以下內(nèi)容:
- 主內(nèi)存(Main Memory):所有線程共享的內(nèi)存區(qū)域,包含了對象的字段、方法和運行時常量池等數(shù)據(jù)。
- 工作內(nèi)存(Working Memory):每個線程擁有自己的工作內(nèi)存,用于存儲主內(nèi)存中的數(shù)據(jù)的副本,線程只能直接操作工作內(nèi)存中的數(shù)據(jù)。
- 內(nèi)存間交互操作:線程通過讀取和寫入操作與主內(nèi)存進行交互。讀操作將數(shù)據(jù)從主內(nèi)存復制到工作內(nèi)存,寫操作將修改后的數(shù)據(jù)刷新到主內(nèi)存。
- 原子性(Atomicity):JMM 保證基本數(shù)據(jù)類型(如 int、long)的讀寫操作具有原子性,即不會被其他線程干擾,保證操作的完整性。
- 可見性(Visibility):JMM 確保一個線程對共享變量的修改對其他線程可見。這意味著一個線程在工作內(nèi)存中修改了數(shù)據(jù)后,必須將最新的數(shù)據(jù)刷新到主內(nèi)存,以便其他線程可以讀取到更新后的數(shù)據(jù)。
- 有序性(Ordering):JMM 保證程序的執(zhí)行順序按照一定的規(guī)則進行,不會出現(xiàn)隨機的重排序現(xiàn)象。這包括了編譯器重排序、處理器重排序和內(nèi)存重排序等。
Java 內(nèi)存模型通過以上規(guī)則和語義,提供了一種統(tǒng)一的內(nèi)存訪問方式,使得多線程程序的行為可預測、可理解,并幫助開發(fā)者編寫正確和高效的多線程代碼。開發(fā)者可以利用 JMM 提供的同步機制(如關(guān)鍵字 volatile、synchronized、Lock 等)來實現(xiàn)線程之間的同步和通信,以確保線程安全和數(shù)據(jù)一致性。
內(nèi)存模型的簡單執(zhí)行示例圖如下:
(1)主內(nèi)存和工作內(nèi)存交互規(guī)范
為了更好的控制主內(nèi)存和本地內(nèi)存的交互,Java 內(nèi)存模型定義了八種操作來實現(xiàn)(以下內(nèi)容只需要簡單了解即可):
- lock(鎖定):作用于主內(nèi)存的變量,把一個變量標識為一條線程獨占狀態(tài)。
- unlock(解鎖):作用于主內(nèi)存變量,把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
- read(讀?。?/strong>:作用于主內(nèi)存變量,把一個變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的 load 動作使用
- load(載入):作用于工作內(nèi)存的變量,它把 read 操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
- use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個變量值傳遞給執(zhí)行引擎,每當虛擬機遇到一個需要使用變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。
- assign(賦值):作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
- store(存儲):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個變量的值傳送到主內(nèi)存中,以便隨后的write的操作。
- write(寫入):作用于主內(nèi)存的變量,它把 store 操作從工作內(nèi)存中一個變量的值傳送到主內(nèi)存的變量中。
“
PS:工作內(nèi)存也就是本地內(nèi)存的意思。
(2)什么是 happens-before 原則?
happens-before(先行發(fā)生)原則是 Java 內(nèi)存模型中定義的用于保證多線程環(huán)境下操作執(zhí)行順序和可見性的一種重要手段。
舉個例子來說,例如 A happens-before B,也就是 A 線程早于 B 線程執(zhí)行,那么 A happens-before B 可以保障以下兩項內(nèi)容:
- 可見性:B 讀取到 A 最新修改的值(通過內(nèi)存屏障)。
- 順序性:編譯器優(yōu)化、處理器重排序等因素不會影響先執(zhí)行 A 再執(zhí)行 B 的順序。