JVM内存模型详解(volatile原理、happen-before规则、内存屏障、可见性与有序性)

JVM内存模型与并发原理解析

前言

JVM内存模型是并发编程的理论基础。为什么多线程下变量修改对其他线程不可见?为什么指令会重排序?volatile如何保证可见性?happen-before原则是什么? 理解主内存与工作内存的交互、指令重排序的本质、volatile的内存屏障、happen-before的传递性,才能掌握Java并发的本质。

摘要

从"多线程下变量修改不可见"的经典bug出发,剖析JVM内存模型的核心原理。通过主内存与工作内存的隔离机制、指令重排序的优化策略、volatile的禁止重排与强制刷新、happen-before的8大规则,揭秘Java内存可见性与有序性的完整保证。配合详细时序图与代码示例,给出并发编程的最佳实践。


一、从变量修改不可见说起

周四上午,哈吉米遇到一个诡异的bug:

场景1:循环停不下来

public class VisibilityTest {
    private static boolean flag = false;
    
    public static void main(String[] args) throws Exception {
        // 线程1:等待flag变为true
        new Thread(() -> {
            System.out.println("线程1启动,等待flag=true...");
            while (!flag) {
                // 死循环,等待flag变为true
            }
            System.out.println("线程1结束");  // 永远执行不到
        }).start();
        
        // 主线程:1秒后设置flag=true
        Thread.sleep(1000);
        System.out.println("主线程:设置flag=true");
        flag = true;
    }
}

// 输出:
线程1启动,等待flag=true...
主线程:设置flag=true
// 线程1永远不会结束!

哈吉米:“为什么线程1看不到flag=true?明明主线程已经设置了!”

南北绿豆:“这是可见性问题——线程1使用的是自己工作内存的副本,看不到主内存的修改。”

哈吉米:“工作内存?主内存?”

阿西噶阿西:“来,我给你讲讲JMM(Java Memory Model)。”


场景2:加了volatile就好了

private static volatile boolean flag = false;  // 加volatile

// 运行结果:
线程1启动,等待flag=true...
主线程:设置flag=true
线程1结束  ← 正常结束了

哈吉米:“加了volatile就能看到了?volatile做了什么?”

南北绿豆:“volatile保证了可见性有序性。”


二、JMM核心原理

2.1 主内存与工作内存

南北绿豆:“JMM定义了线程和内存的交互方式。”

内存模型结构

主内存(Main Memory):
  所有线程共享
  存储变量的主副本
  
工作内存(Working Memory):
  每个线程私有
  存储变量的工作副本
  
交互过程:
  线程读取变量:主内存 → 工作内存 → 线程
  线程修改变量:线程 → 工作内存 → 主内存

内存模型示意图

read
read
write
主内存
flag=true
线程1工作内存
flag=false旧值
线程2工作内存
flag=true新值
线程1
线程2

可见性问题原因

线程1:
  1. 从主内存读取flag=false到工作内存
  2. 在while循环中一直使用工作内存的flag
  3. 不再从主内存读取
  
线程2:
  1. 设置flag=true(修改工作内存)
  2. 刷新到主内存
  
问题:
  线程1的工作内存中flag=false
  看不到主内存的flag=true
  → 可见性问题

哈吉米:“原来每个线程有自己的工作内存,不同步。”


2.2 8种内存交互操作

JMM定义的8种原子操作

操作作用域说明
lock主内存锁定变量,标识为线程独占
unlock主内存解锁变量
read主内存读取变量,传输到工作内存
load工作内存将read的值放入工作内存副本
use工作内存将工作内存的值传给执行引擎
assign工作内存将执行引擎的值赋给工作内存副本
store工作内存将工作内存的值传输到主内存
write主内存将store的值写入主内存变量

变量读写流程

执行引擎 工作内存 主内存 读取变量 read(读取到传输缓冲) load(加载到副本) use(传给执行引擎) 修改变量 assign(赋值给副本) store(传输到缓冲) write(写入主内存) 执行引擎 工作内存 主内存

8种操作的规则

规则:
  1. read和load必须成对出现
  2. store和write必须成对出现
  3. 不允许线程丢弃assign操作(修改了必须同步回主内存)
  4. 不允许线程无原因地同步数据到主内存(没有assign)
  5. 新变量只能在主内存中诞生
  6. 一个变量同一时刻只允许一个线程lock
  7. lock操作会清空工作内存中的变量副本
  8. unlock前必须先将变量同步回主内存

阿西噶阿西:“这8种操作定义了线程和内存的交互规范。”


三、三大特性

3.1 原子性(Atomicity)

定义:一个操作或多个操作,要么全部执行,要么全部不执行。

JMM保证的原子性操作

基本类型的读写(天然原子性):
  int i = 1;     // 原子性
  i = 2;         // 原子性
  
long/double的读写(非原子性):
  long l = 1L;   // 非原子性(32位JVM,分两次写)
  
解决:加volatile
  volatile long l = 1L;  // 原子性

非原子性示例

public class AtomicTest {
    private static int count = 0;
    
    public static void main(String[] args) throws Exception {
        // 10个线程,每个线程+1000次
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    count++;  // 非原子操作
                }
            }).start();
        }
        
        Thread.sleep(3000);
        System.out.println("count = " + count);
        // 期望:10000
        // 实际:9856(每次不一样,都<10000)
    }
}

count++为什么不是原子性?

count++分解为3步:
  
1. 读取count的值(load)
2. 加1(add)
3. 写回count(store)

并发问题:
  线程1:读取count=100
  线程2:读取count=100
  线程1:计算100+1=101,写回
  线程2:计算100+1=101,写回
  
结果:
  count=101(丢失了一次+1)

解决方案

// 方案1:synchronized
private static int count = 0;
public static synchronized void increment() {
    count++;
}

// 方案2:AtomicInteger
private static AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();  // 原子操作

// 方案3:Lock
private static Lock lock = new ReentrantLock();
lock.lock();
try {
    count++;
} finally {
    lock.unlock();
}

南北绿豆:“volatile只保证可见性,不保证原子性,count++要用锁或原子类。”


3.2 可见性(Visibility)

定义:一个线程修改了变量,其他线程能立即看到。

可见性问题原因

原因1:工作内存缓存
  线程使用工作内存的副本,不读主内存
  
原因2:CPU缓存
  变量缓存在CPU缓存(L1、L2、L3)
  其他CPU看不到
  
原因3:编译器优化
  JIT编译器优化,将变量缓存在寄存器

volatile保证可见性

private volatile boolean flag = false;

// volatile的效果:
// 线程1修改flag=true
// → 立即刷新到主内存
// → 通知其他线程的工作内存失效
// → 其他线程重新从主内存读取

volatile内存语义

写volatile变量:
  1. 修改工作内存的volatile变量
  2. 立即刷新到主内存
  3. 使其他CPU缓存失效(MESI协议)
  
读volatile变量:
  1. 使工作内存的副本失效
  2. 从主内存重新读取

volatile时序图

线程1工作内存 主内存 线程2工作内存 flag=false flag=false flag=false 修改flag=true 立即刷新到主内存 flag=true 使工作内存失效 重新从主内存读取 flag=true可见 线程1工作内存 主内存 线程2工作内存

其他保证可见性的方式

1. synchronized
   退出synchronized时,修改刷新到主内存
   进入synchronized时,从主内存读取
   
2. final
   final字段在构造方法结束后,对所有线程可见
   
3. Lock
   unlock时刷新,lock时读取

阿西噶阿西:“volatile是轻量级的同步机制,只保证可见性,不保证原子性。”


3.3 有序性(Ordering)

定义:程序执行的顺序按照代码的先后顺序执行。

指令重排序

为什么会重排序?
  
1. 编译器优化重排序
   编译器在不改变单线程语义的情况下,调整指令顺序
   
2. 指令级并行重排序
   CPU的指令流水线,乱序执行
   
3. 内存系统重排序
   写缓冲区(Store Buffer)导致的乱序

重排序示例

// 代码顺序
int a = 1;  // 1
int b = 2;  // 2
int c = a + b;  // 3

// 可能的执行顺序(重排序)
int b = 2;  // 2
int a = 1;  // 1
int c = a + b;  // 3

// 单线程下,重排序不影响结果

多线程下的问题

public class ReorderTest {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;
    
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 100000; i++) {
            x = 0; y = 0; a = 0; b = 0;
            
            // 线程1
            Thread t1 = new Thread(() -> {
                a = 1;  // 1
                x = b;  // 2
            });
            
            // 线程2
            Thread t2 = new Thread(() -> {
                b = 1;  // 3
                y = a;  // 4
            });
            
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            
            if (x == 0 && y == 0) {
                System.out.println("第" + i + "次:x=0, y=0(发生重排序)");
                break;
            }
        }
    }
}

// 可能的输出:1234次:x=0, y=0(发生重排序)

// 原因:
// 线程1:指令2在指令1之前执行(x=b先于a=1)
// 线程2:指令4在指令3之前执行(y=a先于b=1)
// 结果:x=0, y=0

重排序时序图

线程1 线程2 主内存 正常执行顺序 a=1 b=1 x=b(x=1) y=a(y=1) 结果:x=1 y=1 重排序后 x=b(x=0)先执行 y=a(y=0)先执行 a=1后执行 b=1后执行 结果:x=0 y=0异常 线程1 线程2 主内存

哈吉米:“指令重排序在多线程下会出问题!”

南北绿豆:“对!所以需要禁止重排序。”


四、volatile详解

4.1 volatile的两大特性

特性1:保证可见性

private volatile boolean flag = false;

// 线程1写
flag = true;
// → 立即刷新到主内存
// → 通知其他线程工作内存失效

// 线程2读
while (!flag) { }
// → 从主内存重新读取
// → 能看到flag=true

特性2:禁止指令重排序

public class Singleton {
    private volatile static Singleton instance;  // 必须加volatile
    
    public static Singleton getInstance() {
        if (instance == null) {  // 1
            synchronized (Singleton.class) {
                if (instance == null) {  // 2
                    instance = new Singleton();  // 3
                }
            }
        }
        return instance;
    }
}

// 为什么要加volatile?
// new Singleton()分3步:
//   1. 分配内存
//   2. 初始化对象
//   3. 引用指向内存
//
// 可能重排序为:
//   1. 分配内存
//   3. 引用指向内存(对象还没初始化)
//   2. 初始化对象
//
// 问题:
//   线程1:执行到步骤3,instance!=null,但对象未初始化
//   线程2:判断instance!=null,直接返回
//   线程2拿到未初始化的对象 → 空指针异常
//
// volatile禁止重排序:
//   保证2在3之前执行

4.2 volatile的实现原理

内存屏障(Memory Barrier)

内存屏障作用:
  1. 禁止重排序
  2. 强制刷新缓存

volatile写操作

volatile写前后插入内存屏障:

普通写    ↓
普通写    ↓
StoreStore屏障  ← 禁止上面的写与volatile写重排序
volatile写       ← volatile变量写
StoreLoad屏障   ← 禁止volatile写与下面的读重排序
普通读    ↓

volatile读操作

volatile读前后插入内存屏障:

volatile读       ← volatile变量读
LoadLoad屏障    ← 禁止volatile读与下面的读重排序
普通读    ↓
普通读    ↓
LoadStore屏障   ← 禁止volatile读与下面的写重排序
普通写    ↓

4种内存屏障

屏障类型说明
LoadLoad禁止读-读重排序
StoreStore禁止写-写重排序
LoadStore禁止读-写重排序
StoreLoad禁止写-读重排序(最耗性能)

volatile汇编指令

Java代码:
volatile int v = 1;

编译后的汇编:
mov dword ptr [rsp+4], 1
lock addl $0x0, (%rsp)  ← lock前缀指令

lock前缀的作用:
1. 锁定缓存行,其他CPU无法访问
2. 写操作立即刷新到主内存
3. 使其他CPU缓存失效(MESI协议)

南北绿豆:“volatile通过内存屏障和lock前缀指令,保证可见性和有序性。”


4.3 volatile的使用场景

适用场景

场景1:状态标志
private volatile boolean flag = false;

场景2:双重检查锁(DCL)
private volatile static Singleton instance;

场景3:独立观察
private volatile long count = 0;

不适用场景

场景1:复合操作
count++;  // 不适用(非原子)

场景2:依赖当前值的操作
if (count > 0) {  // 不适用
    count--;
}

volatile vs synchronized

特性volatilesynchronized
原子性❌ 不保证✅ 保证
可见性✅ 保证✅ 保证
有序性✅ 部分保证✅ 保证
阻塞❌ 不阻塞✅ 可能阻塞
性能
适用读多写少复合操作

哈吉米:“volatile是轻量级同步,适合简单的标志位。”


五、happen-before原则

5.1 什么是happen-before?

定义:如果操作A happen-before 操作B,那么A的结果对B可见。

阿西噶阿西:“happen-before是JMM定义的可见性规则。”

8大happen-before规则

规则1:程序顺序规则

int a = 1;  // A
int b = 2;  // B

// A happen-before B
// 在单线程内,A的结果对B可见

规则2:volatile变量规则

volatile boolean flag = false;

// 线程1
flag = true;  // A(volatile写)

// 线程2
if (flag) {   // B(volatile读)
    doSomething();
}

// A happen-before B
// 线程1的修改对线程2可见

规则3:锁规则

synchronized (lock) {
    int a = 1;  // A
}  // unlock

synchronized (lock) {  // lock
    int b = a;  // B(能看到a=1)
}

// unlock happen-before lock
// A的结果对B可见

规则4:线程启动规则

int a = 1;  // A
Thread t = new Thread(() -> {
    int b = a;  // B(能看到a=1)
});
t.start();

// A happen-before B
// 主线程的修改对子线程可见

规则5:线程终止规则

Thread t = new Thread(() -> {
    int a = 1;  // A
});
t.start();
t.join();
int b = a;  // B(能看到a=1,如果a是共享变量)

// A happen-before B
// 子线程的结果对主线程可见

规则6:线程中断规则

// 线程1
thread.interrupt();  // A

// 线程2
if (Thread.interrupted()) {  // B(能看到中断)
    doSomething();
}

// A happen-before B

规则7:对象终结规则

// 构造方法
public User() {
    this.name = "alice";  // A
}

// finalize方法
protected void finalize() {
    String n = this.name;  // B(能看到name="alice")
}

// A happen-before B
// 构造方法的修改对finalize可见

规则8:传递性

int a = 1;        // A
volatile boolean flag = false;
flag = true;      // B(volatile写)

// 线程2
if (flag) {       // C(volatile读)
    int b = a;    // D(能看到a=1)
}

// A happen-before B(程序顺序)
// B happen-before C(volatile规则)
// → A happen-before C(传递性)
// → A happen-before D

happen-before传递链

规则1
规则2
规则8
A: a=1
B: volatile写flag=true
C: volatile读flag
D: 读取a
程序顺序规则
volatile规则
传递性

南北绿豆:“happen-before是可见性的理论基础,8大规则覆盖了所有并发场景。”


六、as-if-serial语义

6.1 单线程的重排序限制

as-if-serial语义

定义:
  不管怎么重排序,单线程的执行结果不能改变

示例

int a = 1;  // 1
int b = 2;  // 2
int c = a + b;  // 3

// 可以重排序:
int b = 2;  // 2
int a = 1;  // 1
int c = a + b;  // 3

// 不能重排序:
int c = a + b;  // 3(错误,a和b还没赋值)
int a = 1;  // 1
int b = 2;  // 2

数据依赖性

有数据依赖:
  a = 1;
  b = a + 1;  // 依赖a,不能重排序到a之前
  
无数据依赖:
  a = 1;
  b = 2;  // 不依赖a,可以重排序

哈吉米:“as-if-serial保证单线程语义不变,但多线程下会有问题。”


七、JMM总结

7.1 核心要点

南北绿豆总结:

  1. JMM定义:线程和主内存的交互规范
  2. 主内存与工作内存:线程有私有的工作内存副本
  3. 三大特性:原子性、可见性、有序性
  4. volatile:保证可见性和有序性,不保证原子性
  5. happen-before:定义可见性规则,8大规则
  6. 内存屏障:禁止重排序、强制刷新缓存

7.2 面试高频问题

阿西噶阿西

问题1:什么是JMM?

JMM(Java Memory Model):
  Java内存模型,定义了线程和主内存的交互规范
  
核心概念:
  - 主内存:所有线程共享,存储变量主副本
  - 工作内存:线程私有,存储变量工作副本
  
交互过程:
  线程读变量:主内存 → 工作内存 → 线程
  线程写变量:线程 → 工作内存 → 主内存
  
问题:
  工作内存的副本可能不同步
  → 可见性问题、有序性问题

问题2:volatile的作用是什么?原理是什么?

作用:
  1. 保证可见性
     一个线程修改,其他线程立即可见
     
  2. 禁止指令重排序
     volatile变量的读写不会与其他操作重排序
  
原理:
  1. 可见性实现:
     - volatile写:立即刷新到主内存
     - volatile读:从主内存重新读取
     - lock前缀指令:使CPU缓存失效
     
  2. 有序性实现:
     - 插入内存屏障(LoadLoad、StoreStore等)
     - 禁止屏障两边的指令重排序
  
注意:
  volatile不保证原子性
  count++ 需要用AtomicInteger或synchronized

问题3:happen-before原则有哪些?

8大规则:
  
1. 程序顺序规则
   单线程内,前面的操作 happen-before 后面的操作
   
2. volatile变量规则
   volatile写 happen-before volatile读
   
3. 锁规则
   unlock happen-before lock
   
4. 线程启动规则
   Thread.start() happen-before 线程内的操作
   
5. 线程终止规则
   线程内的操作 happen-before Thread.join()
   
6. 线程中断规则
   interrupt() happen-before 检测到中断
   
7. 对象终结规则
   构造方法 happen-before finalize()
   
8. 传递性
   A happen-before B,B happen-before C
   → A happen-before C

问题4:volatile和synchronized的区别?

volatile:
  - 轻量级同步
  - 保证可见性和有序性,不保证原子性
  - 不阻塞线程
  - 性能高
  - 适用:状态标志、双重检查锁
  
synchronized:
  - 重量级锁(JDK 6后优化了)
  - 保证原子性、可见性、有序性
  - 可能阻塞线程
  - 性能低(相对volatile)
  - 适用:复合操作、临界区
  
选择:
  - 简单的标志位 → volatile
  - 复合操作(i++) → synchronized或AtomicInteger

问题5:什么是内存屏障?

内存屏障(Memory Barrier):
  CPU指令,用于禁止指令重排序和强制刷新缓存
  
4种屏障:
  1. LoadLoad:禁止读-读重排序
  2. StoreStore:禁止写-写重排序
  3. LoadStore:禁止读-写重排序
  4. StoreLoad:禁止写-读重排序(最耗性能)
  
volatile的内存屏障:
  volatile写:
    StoreStore屏障
    volatile写
    StoreLoad屏障
    
  volatile读:
    volatile读
    LoadLoad屏障
    LoadStore屏障

哈吉米:“掌握了JMM,并发编程的底层原理就清楚了。”


评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值