什么时候需要 volatile?

并不是所有的多线程共享变量都需要 volatile,是否需要 volatile 取决于具体的并发访问场景

1. 什么时候需要 volatile

volatile 主要用于两种情况:

✅ 情况 1:变量在多个线程间可见,但不涉及复合操作

如果一个变量在多个线程间共享,并且线程之间的操作仅仅是读取和写入(无复合操作,如 i++,那么 volatile 可以保证 可见性防止指令重排序

示例 1:线程通知机制

class VolatileExample {
    volatile static boolean flag = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (!flag) { } // 线程会正确看到 flag 的变化
            System.out.println("Flag changed!");
        }).start();

        try { Thread.sleep(1000); } catch (InterruptedException e) {}

        flag = true;  // 另一个线程可以立即感知到这个变化
    }
}

为什么 volatile 必须加上?

  • 保证可见性:如果 flag 不是 volatile,JVM 可能会让线程缓存 flag=false,导致修改后的 flag=true 不会立即对其他线程可见
  • 防止指令重排序volatile 确保 flag = true; 之前的代码不会被重排序到 flag = true; 之后。

✅ 情况 2:防止指令重排序

如果某些变量的赋值顺序必须保持严格一致,volatile 可以阻止编译器和 CPU 对指令的重排序。

示例 2:双重检查锁(DCL)

class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();  // 可能发生重排序
                }
            }
        }
        return instance;
    }
}

为什么 volatile 必须加上?

  • instance = new Singleton(); 这句代码可以分解成:

    memory = allocate();  //  1. 分配内存
    instance = memory;    //  2. 将 instance 指向这块内存(此时对象尚未初始化)
    ctorSingleton(memory); // 3. 调用构造函数,完成初始化
    
  • 可能发生的指令重排序

    • 线程 A 执行 instance = new Singleton();可能先执行步骤 2(instance 指向 memory),再执行步骤 3(调用构造函数)
    • 线程 B 执行 getInstance() 可能会发现 instance 已经不是 null,但实际上对象还未初始化,导致访问异常。
    • volatile 可防止步骤 2 和 3 之间的重排序

2. 什么时候不需要 volatile

volatile 不能保证原子性,所以在以下情况下,它是不够的

❌ 情况 1:变量涉及复合操作(如 i++

如果变量的值需要进行读-改-写这样的操作(即非原子操作),volatile 无法保证线程安全

示例 3:i++ 不是原子操作

class VolatileCounter {
    volatile int count = 0;

    void increase() {
        count++;  // 可能多个线程同时读取 count,导致丢失更新
    }
}

上面的 count++ 并不是一个原子操作,实际上它分解成:

int temp = count;  // 1. 读取 count
temp = temp + 1;   // 2. 计算 count + 1
count = temp;      // 3. 写回 count

如果两个线程同时执行 increase(),它们可能会同时读取 count=5,然后都计算 temp = 6 并写回 count=6,导致 count=7 被丢失

正确做法:使用 synchronizedAtomicInteger

class SafeCounter {
    private AtomicInteger count = new AtomicInteger(0);

    void increase() {
        count.incrementAndGet();  // 线程安全的 i++
    }
}


❌ 情况 2:代码已经被 synchronizedLock 保护

如果变量已经被 synchronizedLock 保护,volatile 就没有必要了,因为锁已经提供了可见性防止重排序

示例 4:synchronized 已经提供了保证

class SafeExample {
    private int a = 0;

    synchronized void set() {
        a = 1;
    }

    synchronized int get() {
        return a;
    }
}

为什么不需要 volatile

  • synchronized 本身已经保证可见性和防止指令重排序,不需要额外的 volatile

❌ 情况 3:变量是只读的

如果某个变量在初始化后不会再修改(例如 final 变量),volatile 就没有意义

示例 5:final 变量天然可见

class Example {
    final int a = 42;  // `final` 变量不会变化,天然线程安全
}

  • final 变量一旦初始化后就不会改变,天然是线程安全的,不需要 volatile

3. 什么时候使用 volatile,什么时候使用 synchronizedLock

需求volatilesynchronized / Lock
变量在多线程间共享✅ 适用✅ 适用
只涉及简单的 读/写✅ 适用✅ 适用,但不推荐
需要保证原子性(i++)❌ 不适用✅ 适用
需要保证复合操作(如检查-修改)❌ 不适用✅ 适用
需要防止指令重排序(如 DCL 单例)✅ 适用✅ 适用
需要更高性能✅ 适用synchronized 开销更大
需要锁住代码块❌ 不适用✅ 适用

4. 结论

✅ 什么时候用 volatile

  1. 仅有单一变量的读/写(不涉及复合操作)(如线程间标志变量 flag)。
  2. 需要防止指令重排序(如双重检查锁 DCL 单例模式)。

❌ 什么时候不能用 volatile

  1. 涉及原子性问题(如 i++count += 1if (x == 0) { x++; })。
  2. 已经使用了 synchronizedLock
  3. 变量不会被修改(如 final 变量)。

如果你对 volatile 还是不太确定,可以问自己:
👉 “这个变量的修改是否是一个复合操作(如 i++)?”

如果答案是 “是的”,那 volatile 不够,你应该使用 synchronizedAtomicInteger

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值