Java 中的原子性、可见性、有序性是多线程编程中最核心的三个特性,也称为“并发三要素”或“happens-before 关系的三大保证”。
它们直接决定了代码在多线程环境下是否能得到“预期”的执行结果。下面用最直白的方式解释三者的含义、为什么会丢失、以及 Java 是如何(或没能)保证它们的。
1. 原子性(Atomicity)
定义:
一个操作(或一组操作)要么全部执行成功,要么全部不执行,不会出现“执行到一半”的中间状态。
最经典的非原子例子(很多人踩过的坑):
intcount=0;// 线程 A 线程 Bcount++;count++;上面看似一行代码,实际 JVM 会分解为:
- 读取 count 的值
- +1
- 把新值写回 count
所以真正的执行序列可能是:
线程A 读 0 线程B 读 0 线程A +1 → 1 线程A 写回 1 线程B +1 → 1 线程B 写回 1最终 count = 1,而不是预期的 2 →丢失更新(非原子)
Java 中真正具有原子性的操作(部分):
| 操作类型 | 是否原子 | 说明 |
|---|---|---|
| 基本类型(除 long/double 外)的读写 | 是 | 32位以内基本类型读写是原子的 |
| long / double 的读写 | 不是(JDK8 前) | 64位拆成两个 32 位,可能看到“半写” |
| volatile long / double | 是(JDK5+) | volatile 保证 long/double 读写原子 |
| 使用 AtomicXXX 类 | 是 | AtomicInteger、AtomicLong 等 |
| synchronized / Lock | 可以做到 | 保护的代码块整体原子 |
小结:
普通 int 的count++不是原子操作
想保证原子性 → 用AtomicInteger、synchronized、ReentrantLock、LongAdder等
2. 可见性(Visibility)
定义:
当一个线程修改了共享变量的值,其他线程立刻能看到最新的值。
经典反例(最常出现的可见性问题):
booleanrunning=true;voidstop(){running=false;// 线程A执行}voidrun(){while(running){// 线程B一直循环// do something}}很多时候线程B永远停不下来,因为它读到的running一直是自己工作内存里的旧值(true),没有从主内存刷新最新值(false)。
原因:
Java 内存模型(JMM)允许线程把变量先写到本地工作内存(CPU 缓存),而不是立刻写回主内存。
其他线程可能长时间从自己的缓存读旧值。
Java 提供可见性保证的方式:
| 方式 | 是否提供可见性 | 额外效果 | 典型使用场景 |
|---|---|---|---|
| volatile | 是 | 禁止指令重排序 | 状态标志、单次发布 |
| synchronized | 是 | 进入/退出同步块刷新 | 保护共享变量的复合操作 |
| Lock(ReentrantLock) | 是 | 同 synchronized | 需要可中断、超时、公平锁场景 |
| final | 是(初始化后) | 禁止重排序 | 不可变对象、安全发布 |
| AtomicXXX | 是 | CAS + volatile 语义 | 原子操作 + 可见性 |
记住一句话:
只要变量被 volatile 修饰、或者在 synchronized 块内读写、或者通过 Atomic 类操作,就有了可见性保证。
3. 有序性(Ordering)
定义:
代码的执行顺序不一定等于程序代码的书写顺序(只要不影响单线程结果,编译器/JVM/CPU 都可以重排序)。
经典反例(双重检查锁单例的陷阱):
classSingleton{privatestaticSingletoninstance;// 非 volatilepublicstaticSingletongetInstance(){if(instance==null){// 第一次检查synchronized(Singleton.class){if(instance==null){// 第二次检查instance=newSingleton();// 问题在这里!}}}returninstance;}}new Singleton()不是一个原子操作,它有三个步骤:
- 分配内存
- 初始化对象(调用构造方法)
- 把引用指向这块内存(instance 指向新对象)
指令重排序后可能出现:
线程 A:1→3→2(先把引用指向内存,再初始化)
线程 B:看到 instance != null,直接返回 → 拿到半初始化对象 → 使用时崩溃或逻辑错误
解决方案(三种主流写法):
// 写法1:volatile(最常用、最清晰)privatestaticvolatileSingletoninstance;// 写法2:静态内部类(推荐,无 volatile 也安全)privatestaticclassHolder{privatestaticfinalSingletonINSTANCE=newSingleton();}publicstaticSingletongetInstance(){returnHolder.INSTANCE;}// 写法3:枚举(最安全、最简洁)publicenumSingleton{INSTANCE;// 其他方法...}Java 提供有序性保证的方式:
| 方式 | 禁止的重排序类型 | 典型使用场景 |
|---|---|---|
| volatile | 写→读、写→写 | DCL 单例、状态标志 |
| synchronized | 进入/退出同步块前后 | 任何需要同步的代码块 |
| final | final 字段初始化 → 后续读 | 不可变对象、安全发布 |
| Happens-before 规则 | 多种情况(详见 JMM 8 条规则) | 理解并发语义的核心 |
快速记忆对比表(面试/实战最常用)
| 特性 | 关注点 | 丢失的表现 | 常见解决方案 | 典型工具/关键字 |
|---|---|---|---|---|
| 原子性 | 操作不可分割 | 读-改-写中间被打断 | synchronized / Lock / AtomicXXX | AtomicInteger、LongAdder |
| 可见性 | 写后其他线程能看到 | 一个线程改了值,另一个线程看不到 | volatile / synchronized / final / Lock | volatile |
| 有序性 | 代码不按书写顺序执行 | new 对象引用先发布,构造未完成 | volatile / final / synchronized | volatile + DCL、静态内部类 |
一句话总结(最常考的表述):
Java 并发编程的三大核心问题:
- 原子性:操作要“要么全做,要么不做”
- 可见性:修改要“让别人看得到”
- 有序性:不要“看起来合理的重排序”把程序搞乱
如果你想继续深入某个点(比如 happens-before 规则完整列表、volatile 底层内存屏障、JMM 模型图解、LongAdder 原理等),可以直接告诉我,我再给你展开更详细的例子和代码。