Java 中的原子性(Atomicity)、可见性(Visibility)和有序性(Ordering) 是并发编程(尤其是多线程编程)中最核心的三大特性,也称为线程安全三大基石。它们直接来源于 Java 内存模型(JMM) 对多线程环境下内存操作行为的约束。
下面用最清晰的方式解释这三个概念、它们为什么会出问题、经典的反例,以及 Java 如何保障它们。
1. 原子性(Atomicity)
定义:一个或多个操作要么全部执行成功,要么全部不执行,中间状态对外不可见,不能被打断。
通俗理解:像“原子”一样不可分割。
Java 中哪些操作天然具有原子性?(JMM 保证)
| 操作类型 | 是否原子 | 说明 |
|---|---|---|
| 基本类型(除 long/double 外)的读取/写入 | 是 | 如 int、boolean、float 的单个读写 |
| long/double 的读写 | 不一定 | 64位拆成两个32位,可能撕裂(极少见) |
| 引用类型的读写 | 是 | 指针本身是原子 |
| i++ / ++i / i += 1 | 否 | 实际是读-改-写三步,非原子 |
| new 对象 | 否 | 分配内存 + 初始化 + 引用赋值 |
经典反例(原子性丢失导致的问题):
private static int count = 0;
public void increment() {
count++; // 看起来一行,实际三步:读 → +1 → 写
}
1000 个线程各执行 1000 次 → 理论 100万,最终值可能只有 98万、99万……(丢失更新)
如何保证原子性?
synchronized块 / 方法Lock(ReentrantLock 等)- 原子类:
AtomicInteger、AtomicLong、AtomicReference、LongAdder等 volatile不保证原子性(只修饰单个变量的读写)
2. 可见性(Visibility)
定义:当一个线程修改了共享变量的值,其他线程能够立刻看到这个最新的值。
为什么会丢失可见性?
现代 CPU + JVM 有多级缓存(寄存器 → L1/L2/L3 → 主存),线程 A 修改了变量后,可能只写到本地工作内存(或 CPU 缓存),没有及时刷回主内存 → 线程 B 还在读旧值。
经典反例(可见性丢失):
private static boolean flag = false;
Thread t1: while (!flag) {} // 可能永远循环
Thread t2: flag = true; // t1 看不到变化
如何保证可见性?
volatile关键字(最轻量)synchronized/Lock(进入/退出监视器时会刷新缓存)final字段(初始化完成后可见)AtomicXXX(内部通常用了 volatile 或 CAS + 内存屏障)Thread.start()、Thread.join()等也会建立 happens-before 关系
3. 有序性(Ordering)
定义:代码的实际执行顺序与程序代码顺序一致(在单线程视角下)。
为什么会乱序?
- 编译器重排序(JIT 优化)
- CPU 乱序执行 + 内存乱序(store buffer、load buffer、失效队列等)
- as-if-serial 语义:单线程内只要最终结果相同,允许重排
经典反例(双检锁单例的经典错误写法):
public class Singleton {
private static Singleton instance; // 没有 volatile
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 非原子:1.分配内存 2.初始化 3.赋值引用
}
}
}
return instance;
}
}
问题:线程 A 执行到 instance = new ... 时,可能先把未初始化的对象引用写到 instance(重排序),线程 B 看到 instance != null,但对象还没构造完 → 使用半初始化对象 → 崩溃或逻辑错误。
正确写法(DCL + volatile):
private static volatile Singleton instance;
volatile 禁止了构造过程的重排序(写 volatile 变量会插入 StoreStore + StoreLoad 屏障)。
三者关系总结表(2026 视角)
| 特性 | 关注点 | 主要问题来源 | 典型解决方案 | 是否被 volatile 解决? | 是否被 synchronized 解决? |
|---|---|---|---|---|---|
| 原子性 | 操作不可分割 | 读-改-写非原子 | AtomicXXX、synchronized、Lock | × | √ |
| 可见性 | 修改对其他线程可见 | 缓存未刷新 | volatile、synchronized、final、AtomicXXX | √ | √ |
| 有序性 | 指令不被重排 | 编译器/CPU/内存乱序 | volatile、synchronized、final(最强保障) | √(禁止部分重排) | √(监视器进入/退出有全屏障) |
核心保障机制:happens-before 关系(JMM 的灵魂)
Java 内存模型不保证“顺序一致性”,而是靠 happens-before 建立部分偏序关系:
如果 A happens-before B,则 A 的所有写操作对 B 可见,且 A 的操作不会被重排到 B 之后。
常见 happens-before 规则(最重要几条):
- 程序顺序规则:单线程内,前面的操作 happens-before 后面的
- 监视器锁规则:解锁 happens-before 后续加锁
- volatile 规则:对 volatile 变量的写 happens-before 后续的读
- 线程启动规则:Thread.start() happens-before 线程内任意操作
- 线程终止规则:线程内所有操作 happens-before Thread.join() 返回
- 传递性:A hb B 且 B hb C ⇒ A hb C
掌握了这三点 + happens-before,你就真正理解了 Java 并发编程的内存一致性基础。
如果你有具体的代码场景(比如想看某个例子为什么会出问题、如何修复),可以贴出来,我可以帮你分析。