并不是所有的多线程共享变量都需要 volatile
,是否需要 volatile
取决于具体的并发访问场景。
volatile
?volatile
主要用于两种情况:
如果一个变量在多个线程间共享,并且线程之间的操作仅仅是读取和写入(无复合操作,如 i++
),那么 volatile
可以保证 可见性 和 防止指令重排序。
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;
之后。如果某些变量的赋值顺序必须保持严格一致,volatile
可以阻止编译器和 CPU 对指令的重排序。
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. 调用构造函数,完成初始化
可能发生的指令重排序:
instance = new Singleton();
时 可能先执行步骤 2(instance 指向 memory),再执行步骤 3(调用构造函数)。getInstance()
可能会发现 instance
已经不是 null
,但实际上对象还未初始化,导致访问异常。volatile
可防止步骤 2 和 3 之间的重排序。volatile
?volatile
不能保证原子性,所以在以下情况下,它是不够的:
i++
)如果变量的值需要进行读-改-写这样的操作(即非原子操作),volatile
无法保证线程安全。
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++
}
}
synchronized
或 Lock
保护如果变量已经被 synchronized
或 Lock
保护,volatile
就没有必要了,因为锁已经提供了可见性和防止重排序。
synchronized
已经提供了保证class SafeExample {
private int a = 0;
synchronized void set() {
a = 1;
}
synchronized int get() {
return a;
}
}
为什么不需要 volatile
?
synchronized
本身已经保证可见性和防止指令重排序,不需要额外的 volatile
。如果某个变量在初始化后不会再修改(例如 final
变量),volatile
就没有意义。
final
变量天然可见class Example {
final int a = 42; // `final` 变量不会变化,天然线程安全
}
final
变量一旦初始化后就不会改变,天然是线程安全的,不需要 volatile
。volatile
,什么时候使用 synchronized
或 Lock
?需求 | volatile |
synchronized / Lock |
---|---|---|
变量在多线程间共享 | ✅ 适用 | ✅ 适用 |
只涉及简单的 读/写 | ✅ 适用 | ✅ 适用,但不推荐 |
需要保证原子性(i++) | ❌ 不适用 | ✅ 适用 |
需要保证复合操作(如检查-修改) | ❌ 不适用 | ✅ 适用 |
需要防止指令重排序(如 DCL 单例) | ✅ 适用 | ✅ 适用 |
需要更高性能 | ✅ 适用 | ❌ synchronized 开销更大 |
需要锁住代码块 | ❌ 不适用 | ✅ 适用 |
volatile
?flag
)。volatile
?i++
、count += 1
、if (x == 0) { x++; }
)。synchronized
或 Lock
。final
变量)。如果你对 volatile
还是不太确定,可以问自己:
“这个变量的修改是否是一个复合操作(如 i++
)?”
如果答案是 “是的”,那 volatile
不够,你应该使用 synchronized
或 AtomicInteger
。