你是否曾经遇到过:明明单线程运行正常的代码,在多线程环境下就出现各种诡异问题?一个线程修改了变量,另一个线程却看不到?代码的执行顺序好像和写的不一样?今天,就让我们彻底揭开Java内存模型的神秘面纱!
1. 引言:为什么需要内存模型?
想象一下这个场景:
java
public class VisibilityProblem {
private static boolean ready = false;
private static int number = 0;
public static void main(String[] args) {
new Thread(() -> {
while (!ready) {
// 空循环,等待ready变为true
}
System.out.println("Number: " + number);
}).start();
number = 42;
ready = true;
}
}
猜猜看:这个程序会输出什么?
你可能会说:"当然是42啊!" 但实际情况是:可能会无限循环,也可能输出0,甚至输出42!
为什么会这样?这就是Java内存模型要解决的核心问题。
2. 计算机体系结构的基础认知
2.1 现代计算机的"记忆系统"
我们的计算机并不是直接操作主内存的,而是有一个复杂的缓存体系:
CPU核心 → L1缓存 → L2缓存 → L3缓存 → 主内存
每个CPU核心都有自己的缓存,这就好比每个工作人员都有自己的笔记本,而不是所有人都直接在同一块黑板上写字。
2.2 Java内存模型的抽象
JMM是一个抽象概念,它定义了:
- 线程如何与主内存交互
- 什么时候写入会对其他线程可见
- 哪些操作顺序可以被重排序
java
// JMM的抽象视图
主内存 (共享)
↑↓
工作内存 (线程私有) ← 每个线程都有自己的工作内存
↑↓
CPU寄存器/缓存
3. 重排序:性能优化的双刃剑
3.1 什么是重排序?
重排序就是编译器和处理器为了优化性能,改变代码的实际执行顺序。
java
// 原始代码
int a = 1;
int b = 2;
int result = a + b;
// 可能的执行顺序(重排序后)
int b = 2; // 先执行
int a = 1; // 后执行
int result = a + b; // 结果仍然是3!
单线程下没问题,因为结果不变。但多线程下就可能出问题!
3.2 重排序的三种类型
- 编译器重排序 - 编译器觉得怎样快就怎样排
- 指令级并行重排序 - CPU同时执行多条指令
- 内存系统重排序 - 缓存机制导致的内存操作乱序
4. Happens-Before:Java的"因果律"
4.1 核心思想
Happens-Before解决了一个根本问题:如何确定一个线程的写操作对另一个线程可见?
4.2 六大规则详解
规则1:程序顺序规则
java
int x = 1; // 操作A
int y = x + 1; // 操作B - 一定能看到x=1
同一个线程内,前面的操作对后面的操作立即可见。
规则2:监视器锁规则
java
synchronized(lock) {
data = value; // 写操作
} // 解锁
// 其他地方
synchronized(lock) {
System.out.println(data); // 一定能看到上面的写入
} // 加锁
解锁操作happens-before后续的加锁操作。
规则3:volatile变量规则
java
volatile boolean flag = false;
int data;
// 线程A
data = 100;
flag = true; // volatile写
// 线程B
if (flag) { // volatile读
System.out.println(data); // 一定能看到100
}
volatile写happens-before后续的volatile读。
规则4:传递性规则
如果 A → B 且 B → C,那么 A → C。
规则5:start()规则
java
// 父线程
config = loadConfig(); // 操作A
Thread child = new Thread(() -> {
// 子线程中一定能看到config的初始化结果
useConfig(config); // 操作B
});
child.start(); // 操作C
A → C → B,因此 A → B。
规则6:join()规则
java
Thread child = new Thread(() -> {
result = compute(); // 操作A
});
child.start();
child.join(); // 操作B
useResult(result); // 操作C - 一定能看到A的结果
A → B → C,因此 A → C。
5. volatile关键字:轻量级同步利器
5.1 volatile的语义
java
public class VolatileExample {
private volatile boolean shutdown = false;
public void shutdown() {
shutdown = true; // 立即可见!
}
public void doWork() {
while (!shutdown) {
// 正常工作
}
}
}
volatile保证:
- 可见性:写操作立即对其他线程可见
- 有序性:禁止指令重排序
- ❌ 不保证原子性:
count++仍然不是线程安全的
5.2 volatile的实现原理
JVM在volatile操作前后插入内存屏障:
写操作前:StoreStore屏障
写操作后:StoreLoad屏障
读操作前:LoadLoad屏障
读操作后:LoadStore屏障
6. 锁的内存语义:重量级但强大
6.1 锁的happens-before关系
java
public class LockExample {
private final Object lock = new Object();
private int sharedData;
public void writer() {
synchronized(lock) {
sharedData = 42; // 临界区内的操作
} // 释放锁
}
public void reader() {
synchronized(lock) { // 获取锁
System.out.println(sharedData); // 一定能看到42
}
}
}
锁释放 → 锁获取 建立了happens-before关系。
6.2 ReentrantLock的实现
java
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
private int count;
public void increment() {
lock.lock();
try {
count++; // 受保护的操作
} finally {
lock.unlock(); // 释放锁,保证可见性
}
}
}
7. final域:不可变性的守护者
7.1 final的内存语义
java
public class FinalExample {
private final int immutableValue;
private int normalValue;
public FinalExample() {
normalValue = 1; // 可能被重排序到构造函数外
immutableValue = 42; // 禁止重排序到构造函数外!
}
}
final保证:对象引用可见时,final域一定已经正确初始化。
7.2 引用类型final的特殊性
java
public class FinalReferenceExample {
private final Map<String, String> config;
public FinalReferenceExample() {
config = new HashMap<>(); // 1. 写final引用
config.put("key", "value"); // 2. 写引用对象成员
// 1和2都不能重排序到构造函数外!
}
}
8. 双重检查锁定:从陷阱到救赎
8.1 错误版本:看似聪明实则危险
java
public class DoubleCheckedLocking {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) { // 第一次检查
synchronized (DoubleCheckedLocking.class) {
if (instance == null) { // 第二次检查
instance = new Instance(); // 🚨 问题根源!
}
}
}
return instance;
}
}
问题根源 :new Instance() 可能被重排序:
- 分配内存空间
- 赋值给instance引用 ← 此时instance不为null但对象未初始化!
- 初始化对象
8.2 正确方案1:volatile修复
java
public class SafeDoubleCheckedLocking {
private volatile static Instance instance; // ✅ 关键修复
public static Instance getInstance() {
if (instance == null) {
synchronized (SafeDoubleCheckedLocking.class) {
if (instance == null) {
instance = new Instance(); // ✅ 现在安全了
}
}
}
return instance;
}
}
8.3 正确方案2:静态内部类(推荐)
java
public class InstanceFactory {
private static class InstanceHolder {
static final Instance INSTANCE = new Instance(); // 由JVM保证线程安全
}
public static Instance getInstance() {
return InstanceHolder.INSTANCE; // 触发类初始化
}
}
JVM类初始化机制:天然线程安全!
9. 处理器差异与JMM的统一
9.1 不同处理器的内存模型
| 处理器 | 内存模型强度 | 允许的重排序 |
|---|---|---|
| x86 | 强 (TSO) | 只允许写-读重排序 |
| ARM/PowerPC | 弱 (RMO) | 允许各种重排序 |
9.2 JMM的桥梁作用
JMM在弱内存模型处理器上插入更多内存屏障,在强内存模型处理器上插入较少屏障,为程序员提供一致的内存模型视图。
java
// 同一段Java代码在不同处理器上:
// x86: 可能只需要1个内存屏障
// ARM: 可能需要4个内存屏障
// 但JMM保证最终行为一致!
10. 实战指南:如何正确编写并发代码
10.1 并发编程的三层境界
第一层:无知者无畏
java
// 🚨 危险!数据竞争!
public class UnsafeCounter {
private int count = 0;
public void increment() { count++; }
public int getCount() { return count; }
}
第二层:过度同步
java
// ✅ 安全但性能差
public class SafeButSlowCounter {
private int count = 0;
public synchronized void increment() { count++; }
public synchronized int getCount() { return count; }
}
第三层:精准同步
java
// ✅ 安全且高效
public class OptimizedCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); }
public int getCount() { return count.get(); }
}
10.2 选择正确的工具
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 状态标志 | volatile boolean |
简单可见性需求 |
| 计数器 | AtomicInteger |
原子性操作 |
| 复杂同步 | ReentrantLock |
灵活性高 |
| 延迟初始化 | 静态内部类 | 简洁安全 |
| 集合操作 | ConcurrentHashMap |
专业工具 |
10.3 常见陷阱与解决方案
陷阱1:认为volatile保证原子性
java
private volatile int count = 0;
count++; // 🚨 不是原子操作!
解决方案:
java
private final AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // ✅ 原子操作
陷阱2:在构造函数中逸出this引用
java
public class ThisEscape {
public ThisEscape() {
BackgroundTask.start(this); // 🚨 危险!对象未完全构造
}
}
解决方案:
java
public class SafeConstruction {
private final Listener listener;
public SafeConstruction() {
listener = new Listener(); // 先完成构造
}
public void start() {
BackgroundTask.start(listener); // 然后安全发布
}
}
11. 总结:掌握JMM,成为并发高手
通过本文的学习,我们应该理解:
- 内存可见性不是自动的,需要正确同步
- Happens-Before是理解Java并发的钥匙
- volatile提供轻量级可见性保证
- 锁提供重量级但功能完整的同步
- final正确使用可以提供初始化安全性
- 避免双重检查锁定陷阱,使用静态内部类方案
记住这个思维模型:
把多线程环境想象成一个团队协作项目:
- 每个线程就像团队成员
- 共享变量就像共享文档
- 同步机制就像会议和邮件通知
- 没有适当的沟通(同步),就会出现信息不一致!
最终建议:
- 优先使用
java.util.concurrent包中的高级工具 - 理解原理,但不轻易手动实现复杂同步
- 测试多线程代码时要考虑各种执行时序
掌握了Java内存模型,你就拥有了编写正确、高效并发程序的能力。现在,是时候让你的多线程代码既安全又高效了!
"并发编程很难,但理解JMM可以让它变得简单一些。"