跳转到正文
Siolin'Log
返回

Java 并发问题:原子性、可见性与有序性

概述

原子性、可见性、有序性,是理解 Java 并发问题的三个核心维度。它们分别回答三个不同的问题:

特性核心问题
原子性一个复合操作会不会被其他线程穿插执行?
可见性一个线程的写入,何时能被其他线程的读取观察到?
有序性其他线程观察到的操作顺序,是否符合程序预期?

它们彼此相关,但不能互相替代。

原子性

在并发语境中,原子性表示一个操作对其他线程而言不可再分:其他线程不能在它的中间步骤插入并观察或修改共享状态。这里强调的是不可分割性,并不意味着操作失败后会自动回滚。

典型问题:count++

class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }
}

count++ 看起来是一行代码,但它不是一个原子操作,大致会被拆成:

1. 读取 count
2. count 加 1
3. 写回 count

假设 count = 0,两个线程同时执行:

线程 A:读取 count,得到 0
线程 B:读取 count,得到 0
线程 A:计算 0 + 1,写入 1
线程 B:计算 0 + 1,写入 1

两个线程分别加了一次,最终结果却是 1,这叫更新丢失。

可见性

可见性描述的是:一个线程写入共享变量后,另一个线程的后续读取是否被保证观察到这次写入。Java 并不笼统保证其他线程会“及时看到最新值”;只有当写操作与读操作之间建立了 happens-before 关系时,写入结果才被保证对读取线程可见。

现代处理器会使用缓存、写缓冲区等机制,编译器和 JIT 也可能进行优化。Java 在语言层面通过内存模型规定线程之间允许观察到什么结果,而不是要求每次读写都直接访问所谓的“主内存”。

线程访问共享变量时的缓存示意图

有序性

有序性表示程序执行顺序是否符合预期。

这里需要区分:

为了提高性能,编译器和 CPU 可能对指令进行重排序。

单线程场景

假设代码是:

int a = 1;
int b = 2;
int c = a + b;

前两行在不影响第三行结果的情况下,可能被交换:

int b = 2;
int a = 1;
int c = a + b;

单线程观察到的结果仍然是 c == 3

编译器、JIT 和处理器可以在不改变单线程执行结果的前提下重排序,这通常称为 as-if-serial。但它不保证其他线程观察到的顺序与源代码顺序完全一致。

多线程场景

class Example {
    private int data;
    private boolean ready;

    public void writer() {
        data = 100;     // 操作 1
        ready = true;   // 操作 2
    }

    public void reader() {
        if (ready) {                  // 操作 3
            System.out.println(data); // 操作 4
        }
    }
}

我们期望线程 B 看到 ready == true 时,data 一定是 100

这段程序存在数据竞争,没有建立跨线程的 happens-before 关系,因此 Java 内存模型允许出现以下情况:

最终线程 B 可能看到:

ready == true
data == 0

如果把 ready 声明为 volatile,对 ready 的写入会 happens-before 后续对它的读取;当线程 B 读到 ready == true 时,之前的 data = 100 也会被保证可见。



上一篇
Java Lock:ReentrantLock、可中断获取与 Condition
下一篇
Java ReadWriteLock:共享读、独占写与锁降级