Skip to content

并发问题的三大根源

理解根源,才能对症下药。

先看三个经典问题

问题一:两个线程同时改一个数,结果不对
问题二:线程 A 改了,线程 B 看不到
问题三:程序卡死了,谁也不动

这三个问题的根源是什么?先从 CPU 和内存的关系说起。

CPU 和内存的速度鸿沟

CPU 计算速度是纳秒级,内存访问速度是百纳秒级。差距 100 倍。

CPU 计算:     1 ns
内存访问:   100 ns  ← 差了 100 倍!

所以 CPU 加了缓存:
CPU → L1/L2/L3 缓存 → 内存

根源一:可见性——线程间数据不可见

问题演示

java
public class VisibilityDemo {

    private static boolean ready = false;
    private static int number = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread reader = new Thread(() -> {
            while (!ready) {
                // 理论上 ready=true 时循环退出
                // 但可能永远看不到 ready=true
            }
            System.out.println(number);  // 可能输出 0
        });

        Thread writer = new Thread(() -> {
            number = 42;
            ready = true;  // 修改后,其他线程可能看不到
        });

        reader.start();
        writer.start();
    }
}

为什么会这样?

线程 A(CPU 0):          线程 B(CPU 1):
number = 42(写 CPU0 缓存)   while (!ready) {
ready = true(写 CPU0 缓存)    // ready 还在 CPU1 缓存里
                                // 永远是 false
                             }

每个 CPU 有自己的缓存,线程在不同的 CPU 上运行时,看不到彼此的修改。

Happens-Before 规则

JVM 通过 Happens-Before 规则保证可见性:

规则含义
程序顺序A 先于 B 执行 → A happens-before B
监视器锁unlock happens-before lock
volatilevolatile 写 happens-before 读
线程启动start() happens-before 线程内操作
线程终止线程操作 happens-before join()
传递性A→B, B→C → A→C
java
// 使用 volatile 建立 happens-before
private volatile boolean ready = false;

// 线程 A:
x = 42;
ready = true;  // volatile 写

// 线程 B:
if (ready) {   // volatile 读
    int y = x;  // 保证能看到 x=42
}

解决可见性问题

java
// 方案1:volatile
private volatile boolean ready = false;

// 方案2:synchronized
private int value;

public synchronized void write(int v) {
    value = v;  // 释放锁 happens-before
}

public synchronized int read() {
    return value;  // 获取锁后读取
}

根源二:有序性——指令重排

CPU 和编译器会对指令重排以提升性能,但重排有规则:

as-if-serial 语义

不管怎么重排,单线程执行结果必须和顺序执行一样。

java
// 源代码
int a = 1;      // 1
int b = 2;      // 2
int c = a + b;  // 3

// 可能被重排为(因为 1 和 2 没有依赖)
int b = 2;      // 2
int a = 1;      // 1
int c = a + b;  // 3

多线程下的重排问题

java
// 线程 A:
ready = true;          // 1
System.out.println(x);  // 2

// 线程 B:
while (!ready) { }      // 等 ready
int y = x;             // 用 x

如果指令重排为:

java
// 线程 A:
System.out.println(x);  // 2 先执行了!
ready = true;          // 1 后执行

线程 B 可能看到 x=0 但 ready=true。

解决有序性问题

java
// 方案1:volatile(阻止重排)
private volatile boolean ready = false;

// 方案2:synchronized
private int x;

public synchronized void writer() {
    x = 42;
    ready = true;  // 这两行不会被重排
}

volatile 的内存屏障

volatile 写:
StoreStore 屏障 → 写操作 → StoreLoad 屏障

volatile 读:
LoadLoad 屏障 → 读操作 → LoadStore 屏障

根源三:原子性——操作不是一步完成

counter++ 不是原子的

java
public class AtomicDemo {

    private int counter = 0;

    public void increment() {
        counter++;  // ❌ 不是原子操作
    }
}

counter++ 实际分解为三步:

1. MOV EAX, [counter]    // 从内存读 counter 到 CPU 寄存器
2. ADD EAX, 1            // 在 CPU 中 +1
3. MOV [counter], EAX    // 写回内存

两个线程同时执行:

线程 A:读 counter=0 → +1 → 还没写回
线程 B:读 counter=0 → +1 → 写回 counter=1
线程 A:写回 counter=1

结果:counter=1,丢了 A 的更新!

解决原子性问题

java
// 方案1:synchronized
private int counter = 0;

public synchronized void increment() {
    counter++;
}

// 方案2:原子类
private AtomicInteger counter = new AtomicInteger(0);

public void increment() {
    counter.incrementAndGet();
}

三大根源总结

根源原因解决
可见性CPU 缓存volatile / synchronized
有序性指令重排volatile / synchronized
原子性操作非一步synchronized / 原子类

synchronized 和 volatile 的区别

volatilesynchronized
可见性
有序性
原子性❌(读 Write 除外)
性能轻量重量

实战:选择正确的同步方式

java
// 场景1:flag 标志位
private volatile boolean running = true;  // ✅ volatile 够了

// 场景2:计数器
private AtomicInteger counter = new AtomicInteger(0);  // ✅ 原子类

// 场景3:复合操作(如 check-then-act)
private int value = 0;

public synchronized void incrementIfPositive() {
    if (value > 0) {  // 检查
        value++;        // 行动
    }                  // ❌ 不能分开,必须 synchronized
}

// 场景4:需要原子性的 flag
private AtomicBoolean processing = new AtomicBoolean(false);

总结

  • 可见性:CPU 缓存导致线程间数据不可见,用 volatile 或 synchronized 解决
  • 有序性:编译器和 CPU 会重排指令,用 volatile 或 synchronized 解决
  • 原子性counter++ 分解为三步,用 synchronized 或原子类解决
  • 理解三大根源,才能正确选择同步方案

基于 VitePress 构建