题目
深入理解Java内存模型(JMM)与并发编程中的可见性问题
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
Java内存模型,volatile关键字,并发编程,可见性,重排序
快速回答
当多个线程访问共享变量时,volatile关键字通过以下机制保证可见性:
- 禁止指令重排序(通过内存屏障)
- 强制线程每次读取都从主内存获取最新值
- 确保写入操作立即刷新到主内存
但需注意:volatile不保证原子性,复合操作仍需同步机制。
解析
问题场景
考虑以下代码在多线程环境下的行为:
public class VisibilityIssue {
// 共享变量
boolean ready = false;
int result = 0;
public void writer() {
result = 42; // 操作1
ready = true; // 操作2
}
public void reader() {
if (ready) { // 操作3
System.out.println(result); // 操作4
}
}
}当线程A执行writer(),线程B执行reader()时,可能输出0而非42。
核心原理
Java内存模型(JMM)规范定义了线程如何与主内存交互:
- 工作内存:每个线程有自己的工作内存(CPU缓存/寄存器副本)
- 可见性问题:操作2可能先于操作1执行(重排序),且修改可能未及时同步到主内存
- happens-before原则:JMM规定的内存可见性保证规则
解决方案:volatile关键字
volatile boolean ready = false; // 添加volatile修饰
volatile通过以下机制工作:
- 内存屏障:
- 写屏障:确保volatile写之前的操作不会被重排序到写之后
- 读屏障:确保volatile读之后的操作不会被重排序到读之前
- 即时可见:写操作直接刷新到主内存,读操作直接从主内存读取
代码验证
public class VolatileDemo {
volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
VolatileDemo demo = new VolatileDemo();
// 线程1:修改flag
new Thread(() -> {
try {
Thread.sleep(100);
demo.flag = true;
System.out.println("Flag set to true");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
// 线程2:检测flag变化
new Thread(() -> {
while (!demo.flag) {
// 空循环等待
}
System.out.println("Flag detected as true");
}).start();
}
}
最佳实践
- 适用场景:状态标志、一次性安全发布(如双重检查锁)
- 限制:
- 不保证复合操作(如i++)的原子性
- 不能替代
synchronized(当需要互斥访问时)
- 替代方案:
AtomicXXX类、显式锁、synchronized
常见错误
- 误用volatile替代同步(如对多个变量做原子更新)
- 忽视64位变量的非原子操作(long/double在32位JVM的非volatile声明)
- 过度使用导致性能下降(volatile读比普通变量慢)
扩展知识
- 内存屏障类型:LoadLoad, StoreStore, LoadStore, StoreLoad
- final字段可见性:正确构造的对象中,final字段初始化对所有线程可见
- happens-before规则:
- 程序顺序规则
- volatile变量规则
- 传递性规则