Java內存模型(JMM)一文透徹理解
JMM核心內容概覽與重要程度評級
在學習JMM前,我們先了解其核心內容體系及重要程度:
| 內容模塊 | 重要程度 | 說明 |
|---|---|---|
| 1. JMM基礎概念 | ???? | 理解JMM的出發點和基本架構 |
| - 硬件基礎與并發挑戰 | ???? | 了解JMM存在的必要性 |
| - 主內存與工作內存 | ???? | JMM的核心抽象概念 |
| 2. 內存間交互操作 | ??? | JMM的基礎操作定義 |
| 3. volatile關鍵字 | ????? | 最常用的同步機制,必須深入掌握 |
| 4. synchronized內存語義 | ????? | 理解鎖的內存效應 |
| 5. happens-before規則 | ????? | JMM的理論核心,解決可見性問題的關鍵 |
| 6. 原子性、可見性、有序性 | ????? | 并發編程的三大核心問題 |
| 7. 安全發布模式 | ???? | 實際開發中的常用技巧 |
| 8. final字段語義 | ??? | 特殊但重要的內存語義 |
| 9. 雙重檢查鎖定問題 | ???? | 經典問題的分析與解決方案 |
| 10. JMM底層實現 | ??? | 理解原理,優化性能 |
接下來,我們將按照重要程度,逐一深入講解各個模塊。
1. JMM是什么?為什么需要JMM?
1.1 JMM的定義與作用
Java內存模型(Java Memory Model, JMM) 是Java虛擬機規范中定義的一種抽象規范,用于屏蔽各種硬件和操作系統的內存訪問差異,實現Java程序在各種平臺下都能達到一致的內存訪問效果。
JMM的核心作用:
- 定義規則:規定多線程環境下變量的訪問方式
- 提供保證:確保在不同平臺上內存訪問行為的一致性
- 允許優化:在保證正確性的前提下允許編譯器和處理器進行優化
1.2 為什么需要JMM:硬件層面的挑戰
現代計算機系統的多層次存儲架構導致了并發編程的三大核心問題:
public class ConcurrencyProblems {
private static boolean ready = false;
private static int number = 0;
public static void main(String[] args) {
// 線程1:數據準備
Thread writer = new Thread(() -> {
number = 42; // 操作1:可能被重排序到操作2之后
ready = true; // 操作2:可能先執行
});
// 線程2:數據處理
Thread reader = new Thread(() -> {
while (!ready) {
// 等待ready變為true
Thread.yield();
}
// 可能輸出0而不是42!
System.out.println("Number: " + number);
});
writer.start();
reader.start();
}
}
問題根源:
- CPU緩存一致性:多核CPU各有緩存,數據更新不同步
- 指令重排序:編譯器和處理器為優化性能重新排序指令
- 內存可見性:一個線程的修改對其他線程不可見
2. JMM的核心架構:主內存與工作內存
JMM通過抽象的內存模型解決上述問題:
工作內存與主內存的交互通過8種原子操作完成:
public class MemoryOperations {
private int sharedValue = 0;
public void operationExample() {
// 1. read: 從主內存讀取變量到傳輸通道
// 2. load: 將read得到的值放入工作內存的變量副本
// 相當于: int temp = sharedValue; (但這是高級語言表示)
// 3. use: 將工作內存中的變量傳遞給執行引擎
int result = sharedValue * 2;
// 4. assign: 將執行引擎的結果賦給工作內存中的變量
sharedValue = result + 1;
// 5. store: 將工作內存中的變量值傳輸到主內存的傳輸通道
// 6. write: 將store獲取的值放入主內存的變量
// 7. lock: 將主內存變量標記為線程獨占狀態
// 8. unlock: 釋放鎖定的變量
}
}
3. ????? volatile關鍵字深度解析
3.1 volatile的語義與保證
volatile是JVM提供的最輕量級的同步機制,提供兩大保證:
- 可見性保證:對volatile變量的寫操作立即對其他線程可見
- 禁止重排序:阻止編譯器和處理器對volatile操作進行重排序
public class VolatileExample {
private volatile boolean flag = false;
private int value = 0;
public void writer() {
value = 42; // 普通寫操作
// StoreStore內存屏障:禁止上面的普通寫與下面的volatile寫重排序
flag = true; // volatile寫操作
// StoreLoad內存屏障:確保volatile寫立即對其他處理器可見
}
public void reader() {
// LoadLoad內存屏障:確保volatile讀之前的所有讀操作已完成
if (flag) { // volatile讀操作
// LoadStore內存屏障:確保volatile讀之后的寫操作不會重排序到讀之前
System.out.println(value); // 保證看到value = 42
}
}
}
3.2 volatile的實現原理
在硬件層面,volatile通過內存屏障指令實現:
public class VolatileBarrier {
private volatile int value;
public void setValue(int newValue) {
this.value = newValue;
// 對應x86匯編代碼:
// mov %eax,0x10(%rsi) ; 將newValue存入value的內存地址
// lock addl $0x0,(%rsp) ; StoreLoad內存屏障(mfence指令)
}
public int getValue() {
// volatile讀在x86上不需要特殊指令
// 因為x86的內存模型已經保證了可見性(TSO模型)
return value;
}
}
內存屏障類型:
- LoadLoad屏障:禁止讀操作重排序
- StoreStore屏障:禁止寫操作重排序
- LoadStore屏障:禁止讀與寫操作重排序
- StoreLoad屏障:禁止寫與讀操作重排序(最重量級)
3.3 volatile的使用場景與限制
適用場景:
- 狀態標志位
- 一次性安全發布
- 獨立觀察(independent observation)
- 開銷較低的讀-寫鎖策略
不適用場景:
- 復合操作(如i++)
- 依賴于當前值的操作(如value = value + 1)
public class VolatileUsage {
// 場景1:狀態標志位
private volatile boolean shutdownRequested;
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while (!shutdownRequested) {
// 執行工作任務
}
}
// 場景2:一次性安全發布
private volatile Resource resource;
public Resource getResource() {
if (resource == null) {
synchronized(this) {
if (resource == null) {
resource = new Resource(); // 安全發布
}
}
}
return resource;
}
}
4. ????? synchronized的內存語義
synchronized不僅提供互斥執行,還提供重要的內存語義:
public class SynchronizedMemory {
private int counter = 0;
private final Object lock = new Object();
public void increment() {
synchronized(lock) {
// monitorenter指令:
// 1. 清空工作內存
// 2. 從主內存重新加載所有共享變量
counter++;
// 臨界區內的操作不會被重排序到臨界區外
}
// monitorexit指令:
// 1. 將工作內存中的修改刷新到主內存
// 2. 釋放鎖
}
public int getCounter() {
synchronized(lock) {
// 獲取鎖會強制從主內存重新讀取變量
return counter;
}
}
}
synchronized的內存語義:
- 進入同步塊:清空工作內存,從主內存重新加載變量
- 退出同步塊:將工作內存中的修改刷新到主內存
- 互斥執行:確保同一時刻只有一個線程執行臨界區代碼
5. ????? happens-before規則
happens-before是JMM的理論核心,定義了操作之間的可見性關系。
5.1 happens-before規則詳解
public class HappensBeforeExample {
private int x = 0;
private volatile boolean v = false;
private int y = 0;
private final Object lock = new Object();
public void demo() {
// 規則1:程序次序規則
x = 1; // 操作A
v = true; // 操作B:A happens-before B
// 規則2:volatile變量規則
if (v) { // 操作C:B happens-before C
y = x; // 操作D:C happens-before D
}
// 規則3:傳遞性規則
// A happens-before B, B happens-before C, C happens-before D
// 因此 A happens-before D
// 規則4:管程鎖定規則
synchronized(lock) { // 加鎖E
x = 2; // 操作F:E happens-before F
} // 解鎖G:F happens-before G
// 規則5:線程啟動規則
Thread t = new Thread(() -> {
System.out.println(x); // 看到x=2
});
t.start(); // start() happens-before 線程中的所有操作
// 規則6:線程終止規則
try {
t.join(); // 線程中的所有操作 happens-before join()返回
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 規則7:對象終結規則
// 對象的構造函數執行結束 happens-before finalize()方法開始
}
}
5.2 happens-before的數學基礎
happens-before關系是一個偏序關系,具有:
- 自反性:A happens-before A
- 反對稱性:如果A happens-before B且B happens-before A,則A=B
- 傳遞性:如果A happens-before B且B happens-before C,則A happens-before C
6. ????? 原子性、可見性、有序性
這是并發編程的三大核心問題,JMM為每個問題提供了解決方案。
6.1 原子性(Atomicity)
原子性是指一個操作不可中斷,要么全部執行成功,要么完全不執行。
public class AtomicityExample {
private int basicType = 0; // 基本類型訪問是原子的
private long longValue = 0L; // long和double可能非原子(但現代JVM通常保證原子性)
private volatile boolean flag = false; // volatile保證單個讀/寫的原子性
// 復合操作不是原子的
public void nonAtomicIncrement() {
basicType++; // 不是原子操作!分解為read-modify-write三步
}
// 保證原子性的方式
private final AtomicInteger atomicInt = new AtomicInteger(0);
private final Object lock = new Object();
private int synchronizedValue = 0;
public void atomicOperations() {
// 方式1:使用原子類
atomicInt.incrementAndGet(); // 原子操作
// 方式2:使用同步
synchronized(lock) {
synchronizedValue++; // 原子操作
}
// 方式3:使用volatile變量(僅適用于特定場景)
flag = true; // 原子操作
}
}
6.2 可見性(Visibility)
可見性是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。
public class VisibilityExample {
private int noVisibility = 0; // 無可見性保證
private volatile boolean hasVisibility = false; // volatile保證可見性
private int synchronizedValue = 0; // synchronized保證可見性
private final Object lock = new Object();
public void demonstrate() {
// 線程1:修改數據
new Thread(() -> {
noVisibility = 42;
hasVisibility = true;
synchronized(lock) {
synchronizedValue = 100;
}
}).start();
// 線程2:讀取數據
new Thread(() -> {
// 可能看不到noVisibility的更新
while (!hasVisibility) {
// 等待hasVisibility變為true
}
// 保證看到noVisibility = 42(因為volatile寫happens-before volatile讀)
synchronized(lock) {
// 保證看到synchronizedValue = 100
}
}).start();
}
}
6.3 有序性(Ordering)
有序性是指程序執行的順序按照代碼的先后順序執行。
public class OrderingExample {
private int x = 0;
private int y = 0;
private volatile boolean ready = false;
public void orderingDemo() {
// 線程1:可能被重排序
new Thread(() -> {
x = 1; // 操作1
y = 2; // 操作2
ready = true; // 操作3:volatile寫,阻止重排序
}).start();
// 線程2
new Thread(() -> {
while (!ready) {
// 等待
}
// 由于volatile的語義,這里保證看到x=1, y=2
// 不會出現y=2但x=0的情況
}).start();
}
}
7. ???? 安全發布模式
安全地發布對象是并發編程中的常見需求,JMM提供了多種模式。
public class SafePublication {
// 方式1:靜態初始化器(最安全)
private static final Resource staticResource = new Resource();
// 方式2:volatile字段
private volatile Resource volatileResource;
public void initVolatileResource() {
volatileResource = new Resource(); // 安全發布
}
// 方式3:final字段
private final Resource finalResource;
public SafePublication() {
this.finalResource = new Resource(); // 安全發布
}
// 方式4:正常鎖保護
private Resource guardedResource;
private final Object lock = new Object();
public void initGuardedResource() {
synchronized(lock) {
if (guardedResource == null) {
guardedResource = new Resource(); // 安全發布
}
}
}
// 方式5:線程安全容器
private final Map<String, Resource> safeMap
= Collections.synchronizedMap(new HashMap<>());
private final ConcurrentMap<String, Resource> concurrentMap
= new ConcurrentHashMap<>();
public void addToSafeMap(String key) {
safeMap.put(key, new Resource()); // 安全發布
}
}
8. ???? 雙重檢查鎖定(DCL)問題與解決方案
雙重檢查鎖定是一個經典的并發模式,但存在陷阱。
8.1 錯誤的DCL實現
public class BrokenDCL {
private static Resource resource; // 沒有volatile!
public static Resource getInstance() {
if (resource == null) { // 第一次檢查(無鎖)
synchronized(BrokenDCL.class) { // 加鎖
if (resource == null) { // 第二次檢查(有鎖)
resource = new Resource(); // 問題所在!
// 可能發生的重排序:
// 1. 分配內存空間
// 2. 將引用指向內存空間(此時resource != null)
// 3. 初始化對象(還未執行)
// 其他線程可能拿到未完全初始化的對象!
}
}
}
return resource;
}
}
8.2 正確的DCL實現
public class CorrectDCL {
// 使用volatile禁止重排序
private static volatile CorrectDCL instance;
private final int value;
private final String name;
private CorrectDCL() {
this.value = 42; // 初始化final字段
this.name = "DCL"; // 初始化普通字段
// 構造函數執行
}
public static CorrectDCL getInstance() {
if (instance == null) { // 第一次檢查(無鎖)
synchronized(CorrectDCL.class) { // 加鎖
if (instance == null) { // 第二次檢查(有鎖)
instance = new CorrectDCL(); // 安全發布
// volatile寫插入內存屏障,確保:
// 1. 所有初始化操作完成
// 2. 初始化結果對其他線程立即可見
}
}
}
return instance;
}
}
9. ??? final字段的內存語義
final字段在并發編程中有特殊的內存語義,提供了安全初始化的保證。
public class FinalFieldExample {
private final int finalValue; // final字段
private int normalValue; // 普通字段
private volatile boolean ready = false;
public FinalFieldExample() {
normalValue = 1; // 普通字段寫入(可能被重排序)
finalValue = 42; // final字段寫入
// JMM在此隱式插入StoreStore內存屏障
// 確保final字段的初始化不會被重排序到構造函數之外
ready = true; // volatile寫
}
public static void reader() {
FinalFieldExample obj = new FinalFieldExample();
// 保證看到finalValue的正確值(42)
// 即使沒有同步,也能看到正確初始化的final字段
int r1 = obj.finalValue;
// 可能看到normalValue的默認值(0)而不是1
// 因為沒有同步保證
int r2 = obj.normalValue;
// 但如果通過volatile讀看到ready=true
// 那么也能保證看到所有字段的正確初始化值
if (obj.ready) {
// 保證看到finalValue=42和normalValue=1
}
}
}
10. JMM在開發中的實際應用
10.1 性能優化建議
- 減少同步范圍:只在必要時使用同步
- 使用volatile代替鎖:當只需要可見性保證時
- 使用線程局部變量:避免共享,消除同步
- 使用并發容器:代替手動同步的容器
10.2 常見陷阱與避免方法
public class CommonConcurrencyMistakes {
// 陷阱1:認為volatile保證原子性
private volatile int count = 0;
public void unsafeIncrement() {
count++; // 不是原子操作!
}
// 解決方案:使用原子類或同步
private final AtomicInteger safeCount = new AtomicInteger(0);
private int synchronizedCount = 0;
private final Object lock = new Object();
public void safeIncrement() {
safeCount.incrementAndGet(); // 方式1:原子類
synchronized(lock) { // 方式2:同步
synchronizedCount++;
}
}
// 陷阱2:誤用雙重檢查鎖定
// 解決方案:使用volatile修飾實例變量
// 陷阱3:依賴線程優先級
// 解決方案:不要依賴線程優先級進行正確性設計
// 陷阱4:在構造函數中啟動線程
public class ProblematicConstructor {
public ProblematicConstructor() {
new Thread(() -> {
// 可能訪問未完全初始化的對象
}).start();
}
}
}
總結
Java內存模型是Java并發編程的基石,它通過定義一系列規則和happens-before關系,在多線程環境中提供了內存可見性、原子性和有序性的保證。
關鍵要點:
- 理解happens-before規則:這是理解線程間操作可見性的核心
- 正確使用volatile:了解其適用場景和限制
- 掌握安全發布模式:確保對象在線程間安全共享
- 避免常見陷阱:識別并避免常見的并發編程錯誤
JMM既是一個規范也是一個工具,正確理解和使用JMM可以幫助我們編寫出既正確又高效的多線程程序。在實際開發中,應該優先使用java.util.concurrent包提供的高級并發工具,它們在大多數情況下都能提供更好的性能和更簡單的編程模型。
?? 如果你喜歡這篇文章,請點贊支持! ?? 同時歡迎關注我的博客,獲取更多精彩內容!
本文來自博客園,作者:佛祖讓我來巡山,轉載請注明原文鏈接:http://www.rzrgm.cn/sun-10387834/p/19102752

浙公網安備 33010602011771號