
一、前言
在 Java 并发编程中,线程安全一直是开发者关注的重点。而 Java 内存模型(Java Memory Model, JMM)正是理解线程之间通信与同步的基础。今天我们就从 JMM 的概念、可见性、原子性、有序性出发,带你深入剖析 JMM 的核心原理。
二、什么是 Java 内存模型?
JMM 并不是一个真实存在的内存结构,而是一组规范,定义了:
- 线程之间如何通过主内存共享变量
- 每个线程的本地工作内存如何与主内存进行交互
JMM 内存结构示意图:
┌─────────────┐
│ 主内存 │
│(共享变量区) │
└─────┬───────┘
│
┌────────┐ ┌───▼─────┐ ┌────────┐
│线程1 │ │线程2 │ │线程3 │
│工作内存 │ │工作内存 │ │工作内存 │
└────────┘ └────────┘ └────────┘
主内存保存所有共享变量,而每个线程都有自己的工作内存,保存了主内存中变量的副本。线程只能操作自己工作内存中的变量,若要相互通信,必须通过主内存。
三、JMM 的三大特性
1. 可见性(Visibility)
当一个线程修改了共享变量的值,其他线程能立刻看到最新值,称为可见性。
解决方案:
- 使用
volatile
修饰变量 - 使用
synchronized
或Lock
同步机制
2. 原子性(Atomicity)
操作要么全部完成,要么全部不做,中间不允许中断。
示例:i++
不是原子操作,它包含:读取 -> 修改 -> 写入
解决方案:
- 使用原子类,如
AtomicInteger
- 使用
synchronized
块保护临界区
3. 有序性(Ordering)
JVM 和 CPU 会进行指令重排序以优化性能,但这可能打破逻辑顺序。
解决方案:
- 使用
volatile
(禁止重排序) - 使用内存屏障(编译器级控制)
四、volatile 是什么?怎么保证可见性与有序性?
volatile
关键字有两个核心作用:
- 保证可见性:修改的值会立即刷新到主内存,其他线程读取到的是最新值。
- 禁止指令重排序:防止重排序破坏语义。
示例:
public class VolatileDemo {
private volatile boolean flag = false;
public void start() {
new Thread(() -> {
while (!flag) {
// do something
}
}).start();
new Thread(() -> {
flag = true;
}).start();
}
}
如果没有 volatile
,子线程可能永远感知不到 flag = true
的变化。
五、JMM 与 synchronized 的关系
synchronized
是 JMM 实现可见性与原子性的重要手段。
- 进入
synchronized
块:工作内存会清空副本,从主内存读取变量 - 离开
synchronized
块:将工作内存的变量刷新到主内存
这使得同步块之间的数据始终保持一致。
六、volatile 和 synchronized 的区别总结
特性 | volatile | synchronized |
---|---|---|
原子性 | ✘ 不保证 | ✔ 保证 |
可见性 | ✔ 保证 | ✔ 保证 |
有序性 | ✔ 禁止指令重排 | ✔ 隐式禁止(通过内存屏障) |
性能开销 | 较小 | 较大 |
适用场景 | 状态标志、双重检查锁等 | 复合操作、临界区访问等 |
七、JMM 在实际中的应用场景
- 单例模式中的双重检查锁:
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
- 自旋等待状态变量变更
八、总结
Java 内存模型(JMM)并不是纸上谈兵,它影响了并发程序中变量共享、线程通信、同步行为的方方面面。掌握 JMM 是理解 Java 并发程序正确性和性能优化的基础。