什么是 Java 中的原子性、可见性和有序性?

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 等)
  • 原子类:AtomicIntegerAtomicLongAtomicReferenceLongAdder
  • 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)

定义:代码的实际执行顺序程序代码顺序一致(在单线程视角下)。

为什么会乱序?

  1. 编译器重排序(JIT 优化)
  2. CPU 乱序执行 + 内存乱序(store buffer、load buffer、失效队列等)
  3. 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 规则(最重要几条):

  1. 程序顺序规则:单线程内,前面的操作 happens-before 后面的
  2. 监视器锁规则:解锁 happens-before 后续加锁
  3. volatile 规则:对 volatile 变量的 happens-before 后续的
  4. 线程启动规则:Thread.start() happens-before 线程内任意操作
  5. 线程终止规则:线程内所有操作 happens-before Thread.join() 返回
  6. 传递性:A hb B 且 B hb C ⇒ A hb C

掌握了这三点 + happens-before,你就真正理解了 Java 并发编程的内存一致性基础。

如果你有具体的代码场景(比如想看某个例子为什么会出问题、如何修复),可以贴出来,我可以帮你分析。

文章已创建 4237

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部