一个"消息滞后"的烦恼
想象你在一个大型办公室工作,同事们共享一个白板记录重要数据。某天你发现:当你更新白板上的数字后,其他同事看到的仍然是旧数据!这就是Java内存模型中的"可见性"问题。今天我们要讲的volatile
关键字,就是解决这个问题的"实时公告系统"。
一、volatile的"双重身份"
1.1 身份一:可见性保证(Visibility)
- 问题场景:
// 没有volatile的情况
boolean isRunning = true;
// 线程A
while(isRunning) {
// 可能永远看不到线程B的修改
}
// 线程B
isRunning = false; // 修改可能对其他线程不可见
- volatile解决方案:
volatile boolean isRunning = true; // 加上volatile
1.2 身份二:禁止指令重排序(Ordering)
- 问题场景:单例模式的双重检查锁定
// 错误的双重检查锁定
class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if(instance == null) { // 第一次检查
synchronized(Singleton.class) {
if(instance == null) { // 第二次检查
instance = new Singleton(); // 可能发生重排序!
}
}
}
return instance;
}
}
- volatile解决方案:
private static volatile Singleton instance; // 加上volatile
二、深入原理:JMM的"交通规则"
2.1 Java内存模型(JMM)基础
2.2 volatile的"特殊通行证"
- 写操作:立即刷新到主内存
- 读操作:直接从主内存读取
- 内存屏障:禁止指令重排序
三、volatile vs synchronized
特性 | volatile | synchronized |
---|---|---|
原子性 | 仅保证单次读/写的原子性 | 保证代码块原子性 |
可见性 | 保证 | 保证 |
有序性 | 部分保证 | 完全保证 |
阻塞 | 不阻塞 | 阻塞 |
适用场景 | 状态标志 | 复合操作 |
四、经典应用场景
4.1 状态标志
volatile boolean shutdownRequested;
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while(!shutdownRequested) {
// 正常工作
}
}
4.2 一次性安全发布
class ResourceHolder {
private volatile Resource resource;
public Resource getResource() {
if(resource == null) {
synchronized(this) {
if(resource == null) {
resource = new Resource();
}
}
}
return resource;
}
}
4.3 低开销的读写锁
class Counter {
private volatile int value;
public int getValue() { // 读操作不需要同步
return value;
}
public synchronized void increment() {
value++; // 写操作需要同步
}
}
五、常见误区警示
-
误区一:认为volatile能替代synchronized
- 事实:volatile不能保证复合操作的原子性
volatile int count = 0; count++; // 这不是原子操作!
-
误区二:过度使用volatile
- 事实:不必要的volatile会降低性能
-
误区三:忽视happens-before关系
- 事实:volatile写操作前的所有修改对后续读操作可见
六、性能考量
- 读操作:volatile变量读取≈普通变量读取
- 写操作:volatile变量写入比普通写入慢2-3倍
- 总体建议:仅在需要时使用
七、终极生活比喻:机场航班显示屏
- 普通变量:像纸质航班表,更新后需要人工分发
- volatile变量:像电子航班屏,所有旅客实时看到最新信息
- synchronized:像登机口,一次只允许一人查看并修改信息
结语:volatile的正确打开方式
记住volatile的适用场景:
- 简单的状态标志
- 单次读/写的独立变量
- 对象的安全发布
不适用场景:
- 需要复合操作(如i++)
- 需要保护多个变量的不变式
最后送大家一个volatile使用口诀:
状态标志用我就对,
单次读写我最配,
复合操作别找我,
该上锁时别怕累!
思考题:如果有一个volatile的int变量,两个线程同时执行i++操作100次,最终i的值会是200吗?为什么?(提示:回忆i++的操作步骤)
下期预告:《Java原子类:无锁并发的秘密武器》敬请期待!