![图片[1]-13 Java内存模型:volatile与并发安全-速优课](http://www.suyouke.com/wp-content/uploads/2026/07/doubao_img_2304x1728_20260722_174942-1-1024x768.png)
Java 内存模型(JMM)定义了 Java 程序中变量的访问规则,特别是在多线程环境下的可见性和有序性。理解 JMM 是编写线程安全代码的基础。本文将深入探讨 JMM 的核心概念、volatile 关键字的作用以及并发安全的保障机制。
一、Java 内存模型概述
JMM 的核心目标
JMM 的主要目标是定义程序中变量的访问规则,确保在多线程环境下的:
- 原子性:操作不可分割
- 可见性:一个线程的修改对其他线程可见
- 有序性:操作按预期顺序执行
JMM 的抽象结构
JMM 将内存分为主内存和工作内存:
┌─────────────────────────────────────────────────────────────┐
│ 主内存(Main Memory) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 共享变量:instanceCount、sharedData、... │ │
│ └─────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 线程1 │ │ 线程2 │ │
│ │ 工作内存 │ │ 工作内存 │ │
│ │ (寄存器、 │ │ (寄存器、 │ │
│ │ 缓存) │ │ 缓存) │ │
│ │ │ │ │ │
│ │ copy of │ │ copy of │ │
│ │ instanceCount│ │ instanceCount│ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
JMM 的操作规则
| 操作 | 说明 |
|---|---|
| lock | 将主内存中的变量锁定 |
| unlock | 解锁主内存中的变量 |
| read | 从主内存读取变量到工作内存 |
| load | 将 read 的值放入工作内存变量副本 |
| use | 使用工作内存中的变量值 |
| assign | 将新值赋值给工作内存中的变量 |
| store | 将工作内存中的变量值写入主内存 |
| write | 将 store 的值写入主内存变量 |
二、volatile 关键字
volatile 的作用
volatile 关键字用于保证变量的可见性和有序性:
- 可见性:当一个线程修改了 volatile 变量的值,新值对其他线程立即可见
- 有序性:禁止指令重排序,保证操作的顺序执行
volatile 的内存语义
写操作语义:
- 将工作内存中的变量值刷新到主内存
- 使其他线程的工作内存中的变量副本失效
读操作语义:
- 从主内存读取变量值到工作内存
- 不使用工作内存中的缓存值
volatile vs synchronized
| 特性 | volatile | synchronized |
|---|---|---|
| 原子性 | 不保证 | 保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 保证 | 保证 |
| 锁机制 | 无 | 有 |
| 性能开销 | 低 | 高 |
volatile 的使用场景
// 场景1:状态标志
public class VolatileFlag {
private volatile boolean running = true;
public void stop() {
running = false;
}
public void run() {
while (running) {
// 业务逻辑
}
}
}
// 场景2:双重检查锁定(DCL)
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
volatile 为什么需要在 DCL 中使用
instance = new Singleton() 不是原子操作,分为三步:
- 分配内存空间
- 初始化对象
- 将引用指向内存空间
如果没有 volatile,可能发生指令重排序:步骤 2 和步骤 3 交换,导致其他线程获取到未初始化的对象。
三、原子性、可见性与有序性
原子性
定义:操作不可分割,要么全部执行,要么全部不执行
Java 中的原子操作:
- 基本类型的读取和赋值(除 long 和 double)
- volatile 变量的读写
- 使用原子类(AtomicInteger、AtomicLong 等)
非原子操作示例:
int count = 0;
count++; // 非原子操作:读取、加1、写入
可见性
定义:一个线程的修改对其他线程可见
保证可见性的方式:
- 使用 volatile 关键字
- 使用 synchronized 关键字
- 使用 Lock 接口
可见性问题示例:
public class VisibilityProblem {
private boolean flag = false; // 没有 volatile
public void writer() {
flag = true; // 修改后可能没有刷新到主内存
}
public void reader() {
while (!flag) { // 可能读取到缓存中的旧值
// 死循环
}
}
}
有序性
定义:操作按预期顺序执行
指令重排序:
- JVM 和 CPU 为了优化性能,可能对指令进行重排序
- 重排序不影响单线程执行结果,但可能影响多线程
保证有序性的方式:
- 使用 volatile 关键字(禁止重排序)
- 使用 synchronized 关键字(建立 happens-before 关系)
- 使用 final 关键字(保证构造器内的初始化完成)
四、happens-before 原则
happens-before 的定义
如果操作 A happens-before 操作 B,那么操作 A 的结果对操作 B 可见,且操作 A 的顺序在操作 B 之前。
常见的 happens-before 关系
| 规则 | 说明 |
|---|---|
| 程序顺序规则 | 同一个线程中,前面的操作 happens-before 后面的操作 |
| volatile 规则 | volatile 写操作 happens-before 后续的 volatile 读操作 |
| 锁规则 | unlock 操作 happens-before 后续的 lock 操作 |
| 传递性 | 如果 A happens-before B,B happens-before C,则 A happens-before C |
| 线程启动规则 | Thread.start() happens-before 线程中的任何操作 |
| 线程终止规则 | 线程中的任何操作 happens-before Thread.join() |
| 对象终结规则 | 对象构造完成 happens-before 调用 finalize() |
happens-before 示例
public class HappensBeforeDemo {
private volatile int a = 0;
private int b = 0;
public void writer() {
a = 1; // 操作1
b = 2; // 操作2
// 根据程序顺序规则:操作1 happens-before 操作2
// 根据 volatile 规则:操作1(volatile写)happens-before 后续的 volatile读
}
public void reader() {
int r1 = a; // 操作3(volatile读)
int r2 = b; // 操作4
// 根据 volatile 规则:操作1 happens-before 操作3
// 根据传递性:操作1 happens-before 操作4
// 所以 r1 = 1 时,r2 一定等于 2
}
}
五、并发安全的保障机制
使用 synchronized
public class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
使用 Lock 接口
public class LockCounter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
使用原子类
public class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
使用 ThreadLocal
public class ThreadLocalCounter {
private static final ThreadLocal<Integer> counter =
ThreadLocal.withInitial(() -> 0);
public void increment() {
counter.set(counter.get() + 1);
}
public int getCount() {
return counter.get();
}
}
六、总结
Java 内存模型是并发编程的基础:
- 内存结构:主内存和工作内存,变量在两者之间传递
- volatile:保证可见性和有序性,不保证原子性
- 三大特性:原子性、可见性、有序性
- happens-before:定义操作之间的顺序关系
- 并发安全:通过 synchronized、Lock、原子类等实现
理解 JMM 的核心概念,对于编写线程安全的代码至关重要。在实际开发中,需要根据具体场景选择合适的并发控制方式。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END








请登录后查看评论内容