引言
在多线程编程中,可见性、原子性、有序性是三个核心问题。Java内存模型(JMM)正是为了解决这些问题而设计的规范。它定义了Java程序在多线程环境下的内存访问规则,是并发编程的基石。本文将带你彻底搞懂JMM的核心概念、内存交互操作以及如何解决并发问题。
为什么要了解Java内存模型?
先看一段看似简单的代码:
java
public class VisibilityDemo {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
// 忙等待
}
System.out.println("线程结束");
}).start();
Thread.sleep(1000);
flag = false;
System.out.println("主线程已将flag设为false");
}
}
预期结果:线程会退出循环,输出"线程结束"
实际结果 :线程永远不会退出!因为子线程看不到主线程修改的flag值。
这就是可见性问题。JMM正是为了解决这类问题而存在的。
JMM内存模型架构图

JMM的核心概念
主内存 vs 工作内存
| 内存类型 | 归属 | 作用 | 类比 |
|---|---|---|---|
| 主内存 | 所有线程共享 | 存储所有共享变量 | 数据库 |
| 工作内存 | 每个线程私有 | 存储变量的副本 | CPU缓存 |
核心规则:
- 所有变量都存储在主内存中
- 每个线程有自己的工作内存,保存主内存变量的副本
- 线程对变量的所有操作都必须在工作内存中进行
- 线程间无法直接访问对方的工作内存
- 线程间通信必须通过主内存中转

JMM的三大特性
原子性(Atomicity)
一个或多个操作要么全部执行且不被中断,要么全部不执行。
Java中的原子操作:
- 基本类型的读取和赋值是原子的(除了long和double)
- 通过
synchronized、Lock、AtomicXXX类保证原子性
java
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicityDemo {
private static int count = 0;
private static AtomicInteger atomicCount = new AtomicInteger(0);
private static final int LOOP = 10000;
public static void main(String[] args) throws InterruptedException {
// 非原子操作演示
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < LOOP; j++) {
// 非原子操作:读-改-写
count++;
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("count++ 结果:" + count + ",预期:" + (10 * LOOP));
// 原子操作演示
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < LOOP; j++) {
// 原子操作
atomicCount.incrementAndGet();
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("atomicCount结果:" + atomicCount.get() + ",预期:" + (10 * LOOP));
}
}
输出结果:
count++ 结果:45623,预期:100000
atomicCount结果:100000,预期:100000
可见性(Visibility)
当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。
保证可见性的方式:
volatile关键字synchronized、Lockfinal关键字(初始化后可见)
java
public class VisibilityDemo {
// 不使用volatile,可能无法看到修改
private static boolean flag = true;
// 使用volatile,保证可见性
// private static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("工作线程启动,flag=" + flag);
int count = 0;
while (flag) {
count++;
// 添加打印或sleep可能让线程看到修改(因为上下文切换)
// 但这不是可靠的方式
}
System.out.println("工作线程结束,共执行了" + count + "次循环");
});
worker.start();
Thread.sleep(1000);
System.out.println("主线程修改flag为false");
flag = false;
worker.join();
System.out.println("程序结束");
}
}
运行结果(不使用volatile):
工作线程启动,flag=true
主线程修改flag为false
(程序卡住,工作线程永远不会退出)
运行结果(使用volatile):
工作线程启动,flag=true
主线程修改flag为false
工作线程结束,共执行了xxxxx次循环
程序结束
有序性(Ordering)
程序执行的顺序按照代码的先后顺序执行。
为什么会乱序?
- 编译器优化:重排指令以提高性能
- CPU乱序执行:为充分利用CPU流水线
- 内存系统重排:缓存导致写操作延迟可见
java
public class OrderingDemo {
private static int a = 0, b = 0;
private static int x = 0, y = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100000; i++) {
a = 0; b = 0; x = 0; y = 0;
Thread t1 = new Thread(() -> {
a = 1; // 操作1
x = b; // 操作2
});
Thread t2 = new Thread(() -> {
b = 1; // 操作3
y = a; // 操作4
});
t1.start();
t2.start();
t1.join();
t2.join();
// 如果完全按顺序执行,不会出现 x=0,y=0 的情况
if (x == 0 && y == 0) {
System.out.println("第" + (i+1) + "次出现重排序:x=" + x + ", y=" + y);
break;
}
}
}
}
输出结果:
第xxx次出现重排序:x=0, y=0
Happens-Before原则
Happens-Before是JMM的核心,它定义了哪些操作必须对后续操作可见。
八大规则
| 规则 | 说明 | 示例 |
|---|---|---|
| 程序次序规则 | 一个线程内,写在前面的操作先行发生于后面的操作 | 代码顺序 |
| 管程锁定规则 | unlock先行发生于后续的lock | synchronized块 |
| volatile规则 | volatile写先行发生于后续的读 | volatile变量 |
| 线程启动规则 | start()先行发生于线程内的所有操作 | Thread.start() |
| 线程终止规则 | 线程所有操作先行发生于join()返回 | Thread.join() |
| 线程中断规则 | interrupt()先行发生于检测到中断 | Thread.interrupted() |
| 对象终结规则 | 对象构造完成先行发生于finalize() | finalize() |
| 传递性规则 | A先行于B,B先行于C,则A先行于C | 链式关系 |
java
public class HappensBeforeDemo {
private int value = 0;
private volatile boolean ready = false;
public void writer() {
value = 42; // 操作1
ready = true; // 操作2(volatile写)
}
public void reader() {
if (ready) { // 操作3(volatile读)
System.out.println(value); // 操作4
// 由于volatile规则,操作1对操作4可见
// 所以value一定是42,不会是0
}
}
public static void main(String[] args) throws InterruptedException {
HappensBeforeDemo demo = new HappensBeforeDemo();
Thread writer = new Thread(demo::writer);
Thread reader = new Thread(demo::reader);
writer.start();
reader.start();
writer.join();
reader.join();
}
}
输出结果:
42
volatile关键字详解
volatile的两层语义
- 保证可见性:写操作立即刷新到主内存,读操作从主内存读取
- 禁止指令重排序:在volatile前后设置内存屏障
java
public class VolatileDemo {
// 单例模式的双重检查锁(经典volatile使用场景)
private static volatile VolatileDemo instance;
private VolatileDemo() {}
public static VolatileDemo getInstance() {
if (instance == null) { // 第一次检查(非同步)
synchronized (VolatileDemo.class) {
if (instance == null) { // 第二次检查(同步)
instance = new VolatileDemo(); // volatile防止指令重排序
}
}
}
return instance;
}
}
volatile vs synchronized
| 特性 | volatile | synchronized |
|---|---|---|
| 保证可见性 | 是 | 是 |
| 保证原子性 | 否(仅单次读写) | 是 |
| 保证有序性 | 是(禁止重排序) | 是 |
| 锁机制 | 无锁 | 重量级锁 |
| 适用场景 | 状态标记、单例双重检查 | 复合操作、代码块 |
代码Demo合集
Demo1:可见性问题演示
java
public class VisibilityProblemDemo {
private static boolean running = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("线程开始");
while (running) {
// 忙等待,JVM可能优化为 while(true)
}
System.out.println("线程结束");
});
t.start();
Thread.sleep(1000);
running = false;
System.out.println("主线程设置running=false");
}
}
输出结果:
线程开始
主线程设置running=false
(程序永远不会结束)
解决方案 :使用volatile修饰running变量
Demo2:volatile解决可见性
java
public class VolatileSolutionDemo {
private static volatile boolean running = true; // 添加volatile
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("线程开始");
while (running) {
// 现在可以正常退出
}
System.out.println("线程结束");
});
t.start();
Thread.sleep(1000);
running = false;
System.out.println("主线程设置running=false");
t.join();
System.out.println("程序正常结束");
}
}
输出结果:
线程开始
主线程设置running=false
线程结束
程序正常结束
Demo3:synchronized解决原子性问题
java
public class SynchronizedAtomicityDemo {
private static int count = 0;
private static final Object lock = new Object();
private static final int THREAD_COUNT = 10;
private static final int LOOP = 10000;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < LOOP; j++) {
synchronized (lock) {
count++; // 现在这个操作是原子的
}
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("最终结果:" + count + ",预期:" + (THREAD_COUNT * LOOP));
}
}
输出结果:
最终结果:100000,预期:100000
最佳实践与常见问题
什么情况下必须使用volatile?
- 状态标志:线程停止标志、初始化完成标志
- 双重检查锁的单例模式
- 独立观察结果:多个线程读取同一个变量,只有一个线程修改
volatile不能保证原子性的示例
java
public class VolatileNotAtomicDemo {
private static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
count++; // 非原子操作,volatile不能保证原子性
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("预期:10000,实际:" + count);
}
}
输出结果:
预期:10000,实际:8654(每次结果不同,小于10000)
JVM参数与调试
bash
# 打印JVM内存模型相关信息
java -XX:+PrintFlagsFinal | grep -i "model"
核心总结
- JMM定义了Java程序在多线程环境下的内存访问规范
- 主内存存储共享变量,工作内存存储线程副本
- 三大特性:原子性、可见性、有序性
- Happens-Before是判断数据竞争和线程安全的核心依据
- volatile保证可见性和有序性,但不保证复合操作的原子性
- synchronized可以同时保证三大特性