可见性
一个线程修改了共享变量,另一个线程却「看不到」。
这听起来违反直觉,但这就是多核 CPU 环境下的常见问题——可见性。
问题的根源:CPU 缓存
为什么会出现可见性问题?因为 CPU 有缓存:
┌─────────────┐ ┌─────────────┐
│ CPU 1 │ │ CPU 2 │
│ ┌───────┐ │ │ ┌───────┐ │
│ │ L1/L2 │ │ │ │ L1/L2 │ │
│ │ Cache │ │ │ │ Cache │ │
│ └───────┘ │ │ └───────┘ │
└──────│──────┘ └──────│──────┘
│ │
└────────┬───────────┘
│
┌────┴────┐
│ 主内存 │
└─────────┘每个 CPU 核心有自己的缓存。线程 A 在 CPU 1 上运行,写入变量 x=1 到缓存;线程 B 在 CPU 2 上运行,读取到的 x 还是 0(因为缓存还没刷新到主内存)。
可见性问题演示
java
public class VisibilityProblemDemo {
private static boolean flag = false;
private static int number = 0;
public static void main(String[] args) throws InterruptedException {
Thread reader = new Thread(() -> {
int count = 0;
while (!flag) {
// 编译器优化:认为 flag 不会变,直接在内联后无限循环
count++;
if (count > 1_000_000) {
// 加一点内存屏障操作,防止完全死循环
Thread.yield();
count = 0;
}
}
System.out.println("读取到 number: " + number);
}, "Reader");
Thread writer = new Thread(() -> {
number = 42;
flag = true; // 写操作
System.out.println("写入完成");
}, "Writer");
writer.start();
reader.start();
reader.join(2000);
if (reader.isAlive()) {
System.out.println("⚠️ Reader 可能陷入无限循环(编译器优化了可见性)");
reader.interrupt();
}
}
}这个问题有两个层面:
- JMM 层面:
flag的修改对 reader 线程可能不可见 - 编译器优化层面:JIT 编译器可能将
while(!flag)优化为while(true)(因为它认为 flag 不会变)
volatile:保证可见性
volatile 关键字告诉编译器:这个变量可能被其他线程修改,不要优化,具体保证两点:
- 写之后立即刷新到主内存:不留在 CPU 缓存
- 读之前立即从主内存读取:不用 CPU 缓存
java
public class VolatileVisibilityDemo {
// volatile 保证可见性
private static volatile boolean flag = false;
private static volatile int number = 0;
public static void main(String[] args) throws InterruptedException {
Thread reader = new Thread(() -> {
while (!flag) {
Thread.yield();
}
// 一定能读取到 number=42
System.out.println("读取到 number: " + number);
}, "Reader");
Thread writer = new Thread(() -> {
number = 42;
flag = true; // 写入后立即刷新到主内存
System.out.println("写入完成");
}, "Writer");
writer.start();
reader.start();
reader.join();
}
}volatile 的 happens-before 语义
volatile 的读写之间有 happens-before 关系:
java
public class HappensBeforeDemo {
private int value = 0;
private volatile boolean ready = false;
// 线程 A(写)
public void writer() {
value = 42; // A1
ready = true; // A2 (volatile 写)
}
// 线程 B(读)
public void reader() {
if (ready) { // B1 (volatile 读)
int x = value; // B2
// B2 一定能看到 A1 的结果
}
}
}happens-before 语义保证:
- A2 happens-before B1(volatile 规则)
- A1 happens-before A2(程序顺序规则)
- B1 happens-before B2(程序顺序规则)
- 传递性:A1 happens-before B2 → x 一定等于 42
synchronized 同样保证可见性
进入 synchronized 会强制刷新 CPU 缓存,退出时会强制将修改写回主内存:
java
public class SynchronizedVisibilityDemo {
private static int number = 0;
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread writer = new Thread(() -> {
synchronized (lock) {
number = 42; // 退出 synchronized 时写回主内存
}
}, "Writer");
Thread reader = new Thread(() -> {
synchronized (lock) {
// 进入 synchronized 时从主内存读取
System.out.println("读取: " + number); // 一定是 42
}
}, "Reader");
writer.start();
reader.start();
}
}经典问题:单例模式中的 volatile
双重检查锁定(Double-Checked Locking)是可见性和有序性的综合考察:
java
public class Singleton {
// volatile 两个作用:
// 1. 防止指令重排序(new Singleton() 的三步可能乱序)
// 2. 保证实例的写入对其他线程可见
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) { // 同步
if (instance == null) { // 第二次检查
instance = new Singleton(); // 创建实例
// 如果没有 volatile,以下顺序可能被重排:
// 1. 分配内存
// 2. 调用构造函数
// 3. 将引用赋值给 instance
// 重排后:1 -> 3 -> 2,导致其他线程看到未初始化的对象
}
}
}
return instance;
}
}可见性 vs 原子性
| 维度 | volatile | synchronized |
|---|---|---|
| 可见性 | ✅ 保证 | ✅ 保证 |
| 原子性 | ❌ 不保证 | ✅ 保证 |
| 性能 | 高 | 低 |
一个常见的误解:以为 volatile 可以替代 synchronized。
java
// ❌ 错误用法
private volatile int counter = 0;
public void increment() {
counter++; // counter++ 不是原子操作!volatile 不保证原子性
}
// ✅ 正确用法
private final AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet(); // 原子操作
}选择决策树
是否需要保证原子性?
├── 否 → 是否需要可见性?
│ ├── 是 → volatile
│ └── 否 → 不需要同步
└── 是 → 是否是单个变量的原子操作?
├── 是 → Atomic* 类
└── 否 → synchronized / ReentrantLock注意事项
- volatile 不保证原子性:
i++这类复合操作不行 - synchronized 同时保证可见性和原子性
- Atomic 类同时保证可见性和原子性*(通过 CAS)
- long/double 的 volatile:JDK 8 之前,64 位变量的读写可能非原子,现在已修复
- 编译器优化:JIT 可能优化掉看似无用的读取,
volatile阻止这种优化
要点回顾
- 可见性问题的根源是 CPU 缓存
volatile保证:写操作立即刷新到主内存,读操作立即从主内存读取volatile的读写之间有 happens-before 关系volatile不保证原子性,复合操作仍需 synchronized 或原子类- 单例模式中
volatile同时防止重排序和可见性问题
相关链接
- 原子性 - 可见性之外的另一个维度
- volatile - volatile 的完整介绍
- Happens-Before - 可见性的理论基础
- 指令重排序 - 有序性问题
