介绍
在 Java 中,volatile
是一个关键字,用于修饰变量,主要解决多线程环境下共享变量的可见性和指令重排序问题。它提供了一种轻量级的同步机制,但需注意其适用场景和限制。只保证单次读写的原子性,不保证复合读写的原子性。
a. 可见性(Visibility)
在多线程环境下,线程会将共享变量从主内存拷贝到自己的工作内存进行操作。若没有同步机制,一个线程对变量的修改可能对其他线程不可见。
volatile 的作用是当变量被声明为 volatile
时,当前线程对该变量的写操作会立即刷新到主内存,其他线程读操作(通过总线嗅探机制)会直接从主内存读取,确保所有线程看到的值一致。
示例
public class VolatileExample {private volatile boolean flag = false; // 测试时移除volatile对比public void start() {// 有 volatile:等待线程能立即退出循环。// 无 volatile:等待线程可能永远无法退出(因读不到修改值)// 等待线程(去除了sleep干扰)new Thread(() -> {System.out.println("等待线程启动");while (!flag) {// 空循环(无任何操作,纯粹依赖可见性)避免任何可能刷新缓存的干扰操作。// 如Thread.sleep也会隐式触发本地缓存刷新(类似 synchronized 的语义)}System.out.println("等待线程检测到flag变化,退出循环");}).start();// 确保等待线程先运行,主线程中短暂休眠,确保等待线程先进入循环try {Thread.sleep(500);}catch (InterruptedException e) {}// 修改线程new Thread(() -> {System.out.println("修改线程启动");flag = true;System.out.println("修改线程已将flag设为true");}).start();}public static void main(String[] args) {new VolatileExample().start();}
}
b. 禁止指令重排序(Ordering)
通常编译器和处理器会对指令进行重新排序以优化性能,但在多线程环境下可能导致意外的结果。volatile禁止指令重排序,它通过插入内存屏障(Memory Barrier),禁止编译器和处理器对 volatile
变量的读写操作进行重排序,确保代码执行顺序符合预期。
示例(单例模式中的双重检查锁机制):
public class Singleton {/*** volatile 关键字在此有两个关键作用:* 1. 可见性保证:确保所有线程看到最新的实例状态* 2. 禁止指令重排序(核心解决点):* - 防止 new Singleton() 的指令重排序* - 对象创建实际包含三个步骤:* ① 分配内存空间* ② 初始化对象(调用构造方法)* ③ 将引用指向内存地址* - 若无 volatile,JVM 可能重排序为 ①→③→②* - 这将导致其他线程拿到未初始化的对象!*/private static volatile Singleton instance;/*** 获取单例实例(线程安全版本)* * 性能说明:相比 synchronized 方法(每次调用都加锁),* 此方案仅在首次创建时同步,后续调用无锁*/public static Singleton getInstance() {// 第一次检查(无锁):避免不必要的同步if (instance == null) {// 同步代码块:确保只有一个线程创建实例synchronized (Singleton.class) {// 第二次检查:防止重复创建if (instance == null) {/*** 关键代码:对象实例化* * 若无 volatile 修饰,此处可能发生指令重排序:* -----------------------------------* | 步骤 | 正常顺序 | 重排序后 |* -----------------------------------* | 1 | 分配内存 | 分配内存 |* | 2 | 初始化对象 | 设置引用(非null)|* | 3 | 设置引用 | 初始化对象 |* -----------------------------------* * 重排序导致后果:* - 当线程A执行了 ①→③ 但未执行②时* - 线程B在第一次检查中看到 instance != null* - 线程B直接返回未初始化的对象 → 程序崩溃!* * volatile 解决方案:* - 在写操作前后插入内存屏障:* StoreStore屏障:确保①→②在③之前完成* StoreLoad屏障:确保③的写操作对其他线程立即可见*/instance = new Singleton();}}}return instance;}// 私有构造器防止外部实例化private Singleton() {// 初始化操作...}
}
2. 常见问题
1、保证单次读写原子性、不保证复合操作原子性
volatile
无法保证复合操作的原子性(如 i++
这类“读-改-写”操作)。此时需使用 synchronized
或原子类(如 AtomicInteger
)。
volatile
仅仅保证单次读写的原子性。原子性需另寻他法(锁或原子类)
示例
public class Counter1 {private volatile int count = 0;public void increment() {count++; // 非原子操作,即使使用 volatile 仍可能线程不安全}
}
// 可修改使用AtomicInteger如下即可保证原子性
public class Counter2 {private volatile AtomicInteger count = new AtomicInteger(0);;public void increment() {count++;}
}
// 可修改使用synchronized如下即可保证原子性
public class Counter3 {private volatile int count = 0;public synchronized void increment() {count++; }
}
volatile只可保证单次读写的原子性,如下
public class VolatileExample4 {// volatile 保证单次读写操作的原子性private volatile int singleOperationValue = 0;// 单次写操作 - 原子性保证public void setValue(int newValue) {singleOperationValue = newValue; // 单次写操作(原子性)}// 单次读操作 - 原子性保证public int getValue() {return singleOperationValue; // 单次读操作(原子性)}public static void main(String[] args) throws InterruptedException {VolatileExample4 demo = new VolatileExample4();ExecutorService executor = Executors.newFixedThreadPool(10);// 测试单次写操作的安全性System.out.println("测试单次写操作:");for (int i = 0; i < 10; i++) {final int value = i;executor.submit(() -> {demo.setValue(value);System.out.println(Thread.currentThread().getName() + " 写入值: " + value);});}executor.awaitTermination(500, TimeUnit.MILLISECONDS);boolean b = demo.getValue() == 9;System.out.println(b+"最终值: " + demo.getValue());System.out.println("所有写入操作完成,没有并发问题\n");// 重置值demo.singleOperationValue = 0;}
}