深入理解Java内存模型:从诡异Bug到优雅解决
你是否曾经遇到过:明明单线程运行正常的代码,在多线程环境下就出现各种诡异问题?一个线程修改了变量,另一个线程却看不到?代码的执行顺序好像和写的不一样?今天,就让我们彻底揭开Java内存模型的神秘面纱!1. 引言:为什么需要内存模型?
想象一下这个场景:
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是一个抽象概念,它定义了:
[*]线程如何与主内存交互
[*]什么时候写入会对其他线程可见
[*]哪些操作顺序可以被重排序
// JMM的抽象视图
主内存 (共享)
↑↓
工作内存 (线程私有) ← 每个线程都有自己的工作内存
↑↓
CPU寄存器/缓存3. 重排序:性能优化的双刃剑
3.1 什么是重排序?
重排序就是编译器和处理器为了优化性能,改变代码的实际执行顺序。
// 原始代码
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:程序顺序规则
int x = 1; // 操作A
int y = x + 1;// 操作B - 一定能看到x=1同一个线程内,前面的操作对后面的操作立即可见。
规则2:监视器锁规则
synchronized(lock) {
data = value;// 写操作
} // 解锁
// 其他地方
synchronized(lock) {
System.out.println(data); // 一定能看到上面的写入
} // 加锁解锁操作happens-before后续的加锁操作。
规则3:volatile变量规则
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()规则
// 父线程
config = loadConfig();// 操作A
Thread child = new Thread(() -> {
// 子线程中一定能看到config的初始化结果
useConfig(config);// 操作B
});
child.start(); // 操作CA → C → B,因此 A → B。
规则6:join()规则
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的语义
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关系
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的实现
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的内存语义
public class FinalExample {
private final int immutableValue;
private int normalValue;
public FinalExample() {
normalValue = 1; // 可能被重排序到构造函数外
immutableValue = 42; // 禁止重排序到构造函数外!
}
}final保证:对象引用可见时,final域一定已经正确初始化。
7.2 引用类型final的特殊性
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 错误版本:看似聪明实则危险
public class DoubleCheckedLocking { private static Instance instance; public static Instance getInstance() { if (instance == null) { // 第一次检查 synchronized (DoubleCheckedLocking.class) { if (instance == null) { // 第二次检查 instance = new Instance(); //
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! 感谢分享,下载保存了,貌似很强大 热心回复! 谢谢分享,辛苦了
页:
[1]