Java內(nèi)存模型JMM深入解析
1. 什么是 JMM?
JMM 的全稱是 Java Memory Model,即 Java 內(nèi)存模型。
簡(jiǎn)單來說,JMM 是一套規(guī)范,它定義了在多線程環(huán)境下,Java 程序中的變量(特別是共享變量)如何被寫入內(nèi)存以及如何從內(nèi)存中讀取的規(guī)則。
關(guān)鍵點(diǎn):
- 它不是 指 Java 程序運(yùn)行時(shí)內(nèi)存區(qū)域的劃分(如堆、棧、方法區(qū))。那是 JVM 內(nèi)存結(jié)構(gòu),是兩個(gè)不同的概念。
- 它是 一個(gè)抽象的概念,是一組規(guī)則和規(guī)范,旨在解決由于多線程訪問共享數(shù)據(jù)而可能引發(fā)的各種問題,如內(nèi)存可見性、原子性、有序性等。
2. 為什么需要 JMM?(JMM 要解決的問題)
在沒有 JMM 約束的情況下,多線程編程會(huì)面臨三大核心難題,這主要是由于現(xiàn)代計(jì)算機(jī)架構(gòu)(如多級(jí)緩存、CPU 指令重排序)造成的。
1. 可見性
- 問題: 一個(gè)線程修改了共享變量的值,另一個(gè)線程不能立即看到這個(gè)修改。
- 原因: 為了提高效率,每個(gè)線程都有自己的工作內(nèi)存(可以理解為CPU高速緩存的一個(gè)抽象),它們會(huì)先將主內(nèi)存中的共享變量拷貝一份到自己的工作內(nèi)存中進(jìn)行操作,操作完成后并不一定會(huì)立即寫回主內(nèi)存。如果線程A修改了值但未刷新到主內(nèi)存,線程B讀取到的就還是舊的值。
- 例子:
// 共享變量
private static boolean flag = false;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true; // 線程A修改flag為true
System.out.println("Flag set to true.");
}).start();
new Thread(() -> {
while (!flag) {
// 線程B可能永遠(yuǎn)無法跳出循環(huán),因?yàn)樗床坏骄€程A對(duì)flag的修改
}
System.out.println("Thread sees flag change.");
}).start();
}在沒有同步措施的情況下,第二個(gè)線程可能會(huì)陷入死循環(huán)。
2. 原子性
- 問題: 一個(gè)或多個(gè)操作,要么全部執(zhí)行成功,要么全部不執(zhí)行,中間不能被任何其他操作中斷。
- 原因: 即使是看似簡(jiǎn)單的操作(如
i++),在底層也是由多個(gè)指令組成的(讀取i,計(jì)算i+1,寫回i)。如果多個(gè)線程同時(shí)執(zhí)行i++,就可能發(fā)生線程A剛讀取完i的值,CPU時(shí)間片就被線程B搶走,線程B也讀取了相同的值并完成寫入,然后線程A再繼續(xù)寫回,最終導(dǎo)致兩次i++結(jié)果只增加了1。 - 例子:
count++就不是原子操作。
3. 有序性
- 問題: 程序執(zhí)行的順序不一定就是代碼編寫的順序。
- 原因: 為了性能優(yōu)化,編譯器和處理器常常會(huì)對(duì)指令進(jìn)行重排序。只要在單線程環(huán)境下,重排序后的結(jié)果與順序執(zhí)行的結(jié)果一致(遵守 as-if-serial 語義),這種優(yōu)化就是被允許的。但在多線程環(huán)境下,重排序可能會(huì)導(dǎo)致意想不到的結(jié)果。
- 例子(經(jīng)典的雙重檢查鎖定單例模式問題):
public class Singleton {
private static Singleton instance; // 沒有volatile
public static Singleton getInstance() {
if (instance == null) { // 第一次檢查
synchronized (Singleton.class) {
if (instance == null) { // 第二次檢查
instance = new Singleton(); // 非原子操作,可能發(fā)生重排序
}
}
}
return instance;
}
}instance = new Singleton() 這行代碼在 JVM 中大致做了三件事:
- 分配對(duì)象的內(nèi)存空間
- 初始化對(duì)象
- 將
instance引用指向這塊內(nèi)存 - 如果步驟2和3被重排序,線程A可能剛執(zhí)行完步驟3(
instance已不為null)但還未初始化對(duì)象時(shí),線程B在第一次檢查if (instance == null)時(shí)發(fā)現(xiàn)不為null,就會(huì)直接返回一個(gè)尚未初始化完成的錯(cuò)誤對(duì)象。
3. JMM 是如何解決這些問題的?
JMM 通過定義一些關(guān)鍵的 關(guān)鍵字 和 規(guī)則 來解決上述問題,主要是圍繞 主內(nèi)存 和 工作內(nèi)存 之間的交互協(xié)議。
核心手段:
synchronized關(guān)鍵字- 原子性:
synchronized塊中的操作具有原子性,同一時(shí)刻只有一個(gè)線程能執(zhí)行。 - 可見性: 當(dāng)線程進(jìn)入
synchronized塊時(shí),會(huì)清空工作內(nèi)存,從主內(nèi)存重新加載變量。退出synchronized塊時(shí),會(huì)把工作內(nèi)存中的修改刷新到主內(nèi)存。 - 有序性: 它通過“一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行 lock 操作”來限制重排序,從而保證有序性??梢钥醋魇菃尉€程執(zhí)行。
- 原子性:
volatile關(guān)鍵字- 可見性: 當(dāng)寫一個(gè)
volatile變量時(shí),JMM 會(huì)立即將該線程工作內(nèi)存中的新值強(qiáng)制刷新到主內(nèi)存。當(dāng)讀一個(gè)volatile變量時(shí),JMM 會(huì)使該線程的工作內(nèi)存無效,從而從主內(nèi)存中重新讀取。 - 有序性: 它通過插入內(nèi)存屏障 來禁止指令重排序。確保了
volatile寫操作之前的任何讀寫操作都不會(huì)被重排序到寫操作之后;volatile讀操作之后的任何讀寫操作都不會(huì)被重排序到讀操作之前。 - 注意:
volatile不保證原子性(例如volatile int i; i++仍然不是原子的)。
- 可見性: 當(dāng)寫一個(gè)
- Happens-Before 原則
- 這是 JMM 中最核心、最復(fù)雜的概念之一。它是一組規(guī)則,用于描述兩個(gè)操作之間的內(nèi)存可見性。如果操作 A Happens-Before 于操作 B,那么 A 操作所做的任何修改對(duì) B 操作都是可見的。
- 程序次序規(guī)則: 在一個(gè)線程內(nèi),書寫在前面的操作先行發(fā)生于書寫在后面的操作。
- 管程鎖定規(guī)則: 一個(gè) unlock 操作先行發(fā)生于后面對(duì)同一個(gè)鎖的 lock 操作。
- volatile變量規(guī)則: 對(duì)一個(gè) volatile 變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作。
- 線程啟動(dòng)規(guī)則: Thread 對(duì)象的
start()方法先行發(fā)生于此線程的每一個(gè)動(dòng)作。 - 線程終止規(guī)則: 線程中的所有操作都先行發(fā)生于對(duì)此線程的終止檢測(cè)。
- 線程中斷規(guī)則: 對(duì)線程
interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生。 - 對(duì)象終結(jié)規(guī)則: 一個(gè)對(duì)象的初始化完成先行發(fā)生于它的
finalize()方法的開始。 - 傳遞性: 如果操作 A 先行發(fā)生于操作 B,操作 B 先行發(fā)生于操作 C,那么操作 A 先行發(fā)生于操作 C。
總結(jié)
| 特性 | 問題描述 | JMM 解決方案 |
|---|---|---|
| 原子性 | 操作被中途打斷 | synchronized |
| 可見性 | 一個(gè)線程的修改對(duì)其他線程不可見 | synchronized, volatile, Happens-Before |
| 有序性 | 指令執(zhí)行順序與代碼順序不一致 | synchronized, volatile, Happens-Before |
一句話總結(jié):
JMM(Java內(nèi)存模型)是一套規(guī)范,它屏蔽了底層硬件內(nèi)存訪問的差異,為 Java 開發(fā)者提供了一套統(tǒng)一的內(nèi)存訪問模型,使得我們?cè)诰帉懚嗑€程程序時(shí),即使在不了解底層硬件細(xì)節(jié)的情況下,也能通過使用 synchronized、volatile 等關(guān)鍵字,編寫出正確、線程安全的代碼。
到此這篇關(guān)于Java內(nèi)存模型JMM深入解析的文章就介紹到這了,更多相關(guān)Java內(nèi)存模型JMM內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Nacos配置中心與本地代碼工程配置文件之間的優(yōu)先級(jí)關(guān)系詳解
本文介紹了Spring Cloud生態(tài)中配置加載原理,強(qiáng)調(diào)Nacos遠(yuǎn)程配置優(yōu)先級(jí)高于本地`application.yml`但低于命令行和環(huán)境變量,覆蓋了多環(huán)境配置、動(dòng)態(tài)刷新配置及安全配置外置等應(yīng)用場(chǎng)景,對(duì)比了Nacos配置中心與本地配置文件的優(yōu)缺點(diǎn),并給出最佳實(shí)踐建議2026-04-04
劍指Offer之Java算法習(xí)題精講數(shù)組與列表的查找及字符串轉(zhuǎn)換
跟著思路走,之后從簡(jiǎn)單題入手,反復(fù)去看,做過之后可能會(huì)忘記,之后再做一次,記不住就反復(fù)做,反復(fù)尋求思路和規(guī)律,慢慢積累就會(huì)發(fā)現(xiàn)質(zhì)的變化2022-03-03
Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(59)
下面小編就為大家?guī)硪黄狫ava基礎(chǔ)的幾道練習(xí)題(分享)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧,希望可以幫到你2021-08-08
SpringMVC 通過ajax 實(shí)現(xiàn)文件上傳的步驟
使用form表單在springmvc 項(xiàng)目中上傳文件,文件上傳成功之后往往會(huì)跳轉(zhuǎn)到其他的頁面,但是有的時(shí)候,文件上傳成功的同時(shí),并不需要進(jìn)行頁面的跳轉(zhuǎn),可以通過ajax來實(shí)現(xiàn)文件的上傳,下面給大家介紹SpringMVC 通過ajax 實(shí)現(xiàn)文件上傳的步驟,感興趣的朋友一起看看吧2025-05-05
學(xué)習(xí)不同 Java.net 語言中類似的函數(shù)結(jié)構(gòu)
這篇文章主要介紹了學(xué)習(xí)不同 Java.net 語言中類似的函數(shù)結(jié)構(gòu),函數(shù)式編程語言包含多個(gè)系列的常見函數(shù)。但開發(fā)人員有時(shí)很難在語言之間進(jìn)行切換,因?yàn)槭煜さ暮瘮?shù)具有不熟悉的名稱。函數(shù)式語言傾向于基于函數(shù)范例來命名這些常見函數(shù)。,需要的朋友可以參考下2019-06-06
spring整合kaptcha驗(yàn)證碼的實(shí)現(xiàn)
這篇文章主要介紹了spring整合kaptcha驗(yàn)證碼的實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-05-05
SpringBoot Redis批量存取數(shù)據(jù)的操作
這篇文章主要介紹了SpringBoot Redis批量存取數(shù)據(jù)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08

