在Java多线程编程里,可见性是指一个线程修改了共享变量的值,其他线程能够立刻感知到这个修改。如果没有合适的处理,线程可能一直使用自己工作内存中的旧值,导致程序逻辑出错。volatile关键字就是Java语言中用来解决这类问题的工具之一,它通过对变量的特殊修饰,让变量的读取和写入都直接面向主内存。

为什么需要volatile保证可见性
现代计算机中,每个线程都有自己的工作内存,变量会被拷贝到工作内存中操作。普通变量写回主内存的时机并不确定,因此其他线程读到的可能还是旧数据。例如下面的代码,其中一个线程修改标志位,另一个线程可能因为看不到变化而一直循环:
public class VisibilityDemo {
// 没有volatile,子线程可能永远看不到stop的变化
private static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!stop) {
// 执行某些轻量操作
}
System.out.println("线程结束");
});
t.start();
Thread.sleep(1000);
stop = true;
System.out.println("主线程修改了stop");
}
}
上面的示例中,由于stop未被volatile修饰,子线程可能一直从自己的工作内存读取值,导致程序无法停止。
如何使用volatile关键字
使用方式非常简单,只需要在变量声明时加上volatile修饰符。这样该变量的读写都会绕过线程工作内存的缓存优化:
public class VisibilityFix {
// 使用volatile保证可见性
private static volatile boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!stop) {
// 循环体为空,仅演示可见性
}
System.out.println("线程正常结束");
});
t.start();
Thread.sleep(1000);
stop = true;
System.out.println("主线程修改了stop,子线程可见");
}
}
加上volatile后,主线程对stop的写入会立刻刷新到主内存,子线程每次读取stop都会从主内存获取,因此能正确感知到变化并退出循环。
volatile的底层原理简述
从JVM层面看,volatile变量的写操作会插入写屏障,读操作会插入读屏障。写屏障保证该变量更新前的所有操作都完成且刷新到主内存;读屏障保证后续读取都从主内存加载。这就避免了指令重排序带来的可见性问题。
volatile的使用限制
需要注意的是,volatile只保证可见性和禁止重排序,并不保证复合操作的原子性。例如下面的计数操作在并发下依然不安全:
public class Counter {
private static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
count++; // 非原子操作:读、改、写
}
};
Thread a = new Thread(task);
Thread b = new Thread(task);
a.start();
b.start();
a.join();
b.join();
System.out.println("最终count=" + count); // 可能小于2000
}
}
如果需要对count进行安全的并发递增,应使用synchronized块或AtomicInteger等原子类。
适用场景总结
- 状态标志位控制,如上面示例中的stop变量
- 单次赋值且被多个线程读取的配置项
- 双重检查锁定模式中的实例引用声明
总之,volatile是Java并发编程中轻量且实用的可见性解决方案,理解它的能力和边界,能帮助开发者写出更高效且正确的多线程程序。