概述
原子性、可见性、有序性,是理解 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 实际执行顺序
- 其他线程观察到的顺序
为了提高性能,编译器和 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 内存模型允许出现以下情况:
- 指令发生重排序;
ready的新值已经可见;data的新值仍不可见。
最终线程 B 可能看到:
ready == true
data == 0
如果把 ready 声明为 volatile,对 ready 的写入会 happens-before 后续对它的读取;当线程 B 读到 ready == true 时,之前的 data = 100 也会被保证可见。