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