什么时候需要 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 之间的重排序。
- 线程 A 执行
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
被丢失。
正确做法:使用 synchronized
或 AtomicInteger
class SafeCounter {
private AtomicInteger count = new AtomicInteger(0);
void increase() {
count.incrementAndGet(); // 线程安全的 i++
}
}
❌ 情况 2:代码已经被 synchronized
或 Lock
保护
如果变量已经被 synchronized
或 Lock
保护,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
,什么时候使用 synchronized
或 Lock
?
需求 | volatile | synchronized / Lock |
---|---|---|
变量在多线程间共享 | ✅ 适用 | ✅ 适用 |
只涉及简单的 读/写 | ✅ 适用 | ✅ 适用,但不推荐 |
需要保证原子性(i++) | ❌ 不适用 | ✅ 适用 |
需要保证复合操作(如检查-修改) | ❌ 不适用 | ✅ 适用 |
需要防止指令重排序(如 DCL 单例) | ✅ 适用 | ✅ 适用 |
需要更高性能 | ✅ 适用 | ❌ synchronized 开销更大 |
需要锁住代码块 | ❌ 不适用 | ✅ 适用 |
4. 结论
✅ 什么时候用 volatile
?
- 仅有单一变量的读/写(不涉及复合操作)(如线程间标志变量
flag
)。 - 需要防止指令重排序(如双重检查锁 DCL 单例模式)。
❌ 什么时候不能用 volatile
?
- 涉及原子性问题(如
i++
、count += 1
、if (x == 0) { x++; }
)。 - 已经使用了
synchronized
或Lock
。 - 变量不会被修改(如
final
变量)。
如果你对 volatile
还是不太确定,可以问自己:
👉 “这个变量的修改是否是一个复合操作(如 i++
)?”
如果答案是 “是的”,那 volatile
不够,你应该使用 synchronized
或 AtomicInteger
。