在多线程编程中,最让人头疼的问题莫过于“数据不一致”:明明修改了一个变量,其他线程却看不到;或者指令执行顺序和代码顺序不一样,导致逻辑错误。这些现象背后,都和Java内存模型(JMM,Java Memory Model)密切相关。本文通过通俗比喻+代码示例,带你彻底理解JMM如何解决多线程的数据可见性、有序性和原子性问题!
一、JMM是什么?先搞懂“内存分层架构”
🏢 生活类比:程序员协作改文档
假设你和同事共同维护一个Excel表格(主内存):
- 每个人会先把表格复制一份到自己电脑(工作内存);
- 修改自己的副本后,再同步回主表格(写回主内存);
- 同事的电脑不会实时看到你的修改,除非你主动同步。
JMM就是这套“主内存→工作内存”的规则:
- 主内存:所有线程共享的公共内存,存储共享变量(如静态变量、对象实例变量);
- 工作内存:每个线程的私有内存,存储主内存变量的副本(线程对变量的读写必须通过工作内存,不能直接操作主内存)。
📍 关键规则:
- 线程间无法直接访问对方的工作内存,必须通过主内存间接通信;
- 变量的读取/写入必须先从主内存→工作内存(读),或工作内存→主内存(写)。
二、三大核心问题:可见性、有序性、原子性
1. 可见性问题:线程间变量修改不透明
🌰 代码示例(错误示范):
public class VisibilityProblem {
static int x = 0;
static boolean running = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (running) { // 线程1:检查running是否为true
x++;
}
System.out.println("线程1结束,x=" + x);
}).start();
Thread.sleep(100);
running = false; // 线程2:修改running为false
System.out.println("主线程修改running为false");
}
}
现象:
- 主线程修改
running=false
后,线程1可能永远不结束(因为线程1的工作内存中running
还是true,没同步主内存)。
✨ 原因:
线程1和线程2的工作内存中,running
的副本未及时同步到主内存,导致可见性问题。
2. 有序性问题:指令重排序导致逻辑混乱
🌰 生活类比:
你写代码是“烧水→泡茶”,但编译器为了优化,可能变成“泡茶→烧水”(如果不依赖顺序)。单线程没问题,多线程时若另一个线程检查“水是否烧开”,就会得到错误结果。
💻 代码示例(双重检查锁定错误):
public class Singleton {
static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 可能被重排序为①分配内存 ②设置引用 ③初始化对象
}
}
}
return instance;
}
}
问题:
instance = new Singleton()
可能被重排序为:
- 分配内存空间(instance指向空对象);
- 将instance引用赋值给变量(此时instance非null,但对象未初始化);
- 执行构造函数(初始化对象)。
若线程A执行到步骤2时,线程B检查instance != null
,会返回未初始化的对象,导致空指针异常。
3. 原子性问题:操作被中间打断
🌰 经典案例:
int i = 0;
i++; // 非原子操作,实际是3步:读取i→加1→写入i
多线程同时执行i++
时,可能出现值覆盖(如两个线程读到i=0,都加1后写入,最终i=1而不是2)。
三、JMM如何解决三大问题?关键靠这3招!
1. 可见性保障:volatile与synchronized
🔑 volatile关键字:
- 作用:强制线程每次读取变量时从主内存获取最新值,写入时立即同步到主内存;
- 原理:通过插入“内存屏障”禁止指令重排序,保证可见性(但不保证原子性)。
📝 修正可见性问题代码:
static volatile boolean running = true; // 添加volatile
🚪 synchronized关键字:
- 作用:加锁时,先清空工作内存,从主内存重新读取变量(保证可见性);
- 解锁时,将工作内存的修改同步回主内存(保证可见性)。
2. 有序性保障:禁止指令重排序
🔄 内存屏障(Memory Barrier):
JMM通过插入内存屏障,告诉编译器和CPU:
- 屏障左边的指令不能重排序到右边;
- 屏障右边的指令不能重排序到左边。
🌟 volatile的特殊规则:
- 写volatile变量前,插入写屏障(保证前面的操作先于写操作执行);
- 读volatile变量后,插入读屏障(保证读操作后于后面的操作执行)。
修正双重检查锁定:
static volatile Singleton instance; // 添加volatile禁止重排序
3. 原子性保障:synchronized与CAS
🔒 synchronized:
保证代码块内的操作是原子的(同一时间只有一个线程执行)。
✅ CAS(Compare-And-Swap):
通过CPU原子指令(如cmpxchg
)保证单个变量操作的原子性(如AtomicInteger
的incrementAndGet
)。
四、happens-before原则:线程间的“顺序契约”
JMM定义了一套规则,只要满足这些规则,就认为操作之间存在“happens-before”关系(即前一个操作的结果对后一个操作可见)。
📑 六大规则(必记!):
- 程序顺序规则:单线程内,前面的操作happens-before后面的操作(如
a=1; b=2
,a=1
的结果对b=2
可见); - 监视器锁规则:解锁操作happens-before后续的加锁操作(如线程A释放锁,线程B加锁后能看到A的修改);
- volatile变量规则:对volatile变量的写操作happens-before后续的读操作(保证可见性和有序性);
- 传递性:若A happens-before B,B happens-before C,则A happens-before C;
- 线程启动规则:
start()
方法happens-before线程内的第一个操作(主线程启动子线程,子线程能看到启动前的变量); - 线程终止规则:线程内的最后一个操作happens-before
join()
返回(主线程join()
子线程后,能看到子线程的所有修改)。
🌰 应用案例:
volatile int x = 0;
int y = 0;
// 线程A
x = 1; // 操作1
y = 2; // 操作2(根据程序顺序规则,操作1 happens-before操作2)
// 线程B
int a = y; // 操作3
int b = x; // 操作4(根据volatile规则,操作1 happens-before操作4,所以b一定是1)
线程B中,b
一定是1(因为操作1对操作4可见),但a
可能是0或2(操作2和操作3无happens-before关系)。
五、实战避坑:如何写出线程安全的代码?
1. 可见性问题解决方案:
- 对状态标记变量(如
boolean running
),用volatile
; - 对复杂操作(如
i++
),用synchronized
或AtomicInteger
。
2. 有序性问题解决方案:
- 对需要禁止重排序的变量,用
volatile
(如单例模式中的实例); - 避免在多线程中使用未经同步的共享变量。
3. 原子性问题解决方案:
- 简单操作:用
Atomic
原子类(如AtomicInteger
); - 复合操作:用
synchronized
包裹代码块(如同时修改多个变量)。
🌟 最佳实践:
// 正确的单例模式(双重检查锁定)
public class SafeSingleton {
private static volatile SafeSingleton instance; // 关键:volatile禁止重排序
private SafeSingleton() {}
public static SafeSingleton getInstance() {
if (instance == null) {
synchronized (SafeSingleton.class) {
if (instance == null) {
instance = new SafeSingleton(); // 不会被重排序为“半初始化”状态
}
}
}
return instance;
}
}
六、总结:JMM的核心逻辑图
七、常见误区与解答
❓ 误区1:volatile能保证原子性吗?
答:不能!volatile int i; i++
仍是非原子操作(3步:读→改→写),多线程下可能丢失更新。
❓ 误区2:happens-before是时间先后关系吗?
答:不是!它是JMM定义的逻辑顺序,时间先后满足happens-before,但反之不一定(如两个无关线程的操作,时间上有先后,但无happens-before关系)。
❓ 误区3:所有共享变量都要用volatile吗?
答:不是!volatile适合“单个变量的可见性和禁止重排序”,复杂场景用synchronized
或ReentrantLock
(保证原子性和可见性)。
八、JMM的终极目标:在性能与安全间找平衡
JMM允许编译器和CPU进行合理的优化(如指令重排序),以提升性能,同时通过规则保证多线程程序的正确性。理解JMM,就是理解如何在“高效”和“正确”之间找到平衡:
- 简单场景:用
volatile
解决可见性,Atomic
类解决原子性; - 复杂场景:用
synchronized
或锁保证原子性、可见性和有序性; - 永远记住:多线程代码的正确性高于性能,先保证正确,再优化!
掌握JMM,就能看透多线程数据不一致的本质,写出稳定可靠的并发程序~ 💪
觉得有帮助的话,点赞收藏不迷路!下期聊聊“Java并发编程:从线程安全到性能优化,实战案例解析”~