1214 字
6 分钟
JMM、volatile 与 synchronized:可见性、原子性和有序性

一个线程的写入何时被另一个线程看见,哪些操作不可分割,都需要规则。JMM 是 Java 对线程间内存行为的规范,不等于 JVM 堆栈划分。

三个问题#

  • 原子性:操作是否不可分割。count++ 包含读取、计算、写入,不是天然原子操作。
  • 可见性:一个线程修改共享数据后,其他线程能否按同步规则看到结果。
  • 有序性:编译器和处理器可以重排序,但必须遵守内存模型规定的可观察行为。

工作内存是解释线程交互的抽象,不代表每个线程都实际维护一份完整的主内存副本。

happens-before#

happens-before 表示规范承诺的可见性和顺序关系,不只是现实时间上先发生。常用规则是:

  1. 同一线程内,前面的操作 happens-before 后面的操作。
  2. 对一个监视器的解锁 happens-before 后续对同一监视器的加锁。
  3. 对 volatile 变量的写 happens-before 后续对该变量的读。
  4. 调用 start 前的操作对启动后的线程可见。
  5. 线程中的操作对成功 join 等待它结束的线程可见。
  6. 关系具有传递性。

重点是能用规则解释为何可见,不需要背所有规范条文。

volatile 能保证什么?#

volatile 适合状态标记和满足条件的安全发布,建立相关读写的可见性与排序约束,不能让多个步骤组成原子操作。

volatile boolean running = true;
volatile int count = 0;
// running 可以作为协作停止标记
// count++ 仍可能丢失更新

计数累加可以使用 AtomicInteger,或用同一把锁保护。volatile 引用也不自动保护对象内部的并发修改。

synchronized 锁住什么?#

实例同步方法通常锁 this;静态同步方法锁对应 Class 对象;同步块锁括号中指定对象。只有竞争同一个监视器的操作才会互斥。

锁同时提供互斥和同步可见性,但共享状态的相关读写要遵守同一套协议。写时加锁,读时完全无同步,不能仅靠写端加锁保证正确。

双重检查单例为什么需要 volatile?#

双重检查在锁外读取实例引用,锁内再确认并创建。实例引用使用 volatile,保证安全发布,避免其他线程看到引用后不能正确观察已初始化状态。

final class DclSingleton {
private static volatile DclSingleton instance;
private DclSingleton() {}
public static DclSingleton getInstance() {
if (instance == null) {
synchronized (DclSingleton.class) {
if (instance == null) {
instance = new DclSingleton();
}
}
}
return instance;
}
}

外层检查避免每次调用都加锁,内层检查防止多个线程依次进入锁后重复创建,volatile 负责实例的安全发布。缺少任意一层相应保障,都不能只凭“用了 synchronized”就认定这个模式正确。

常见线程安全单例怎么选?#

方式机制创建时机与取舍
饿汉式静态字段在类初始化中创建,利用类初始化保证类初始化时创建,不是必然在应用启动那一刻
双重检查锁控制创建,volatile 安全发布按需创建,但手写逻辑较多
静态内部类内部 Holder 的静态字段通过类初始化创建常规访问 getInstance 时触发,简洁的延迟创建方案
枚举枚举常量与类初始化机制常规枚举语义下单例身份稳定,并有序列化等方面优势
final class HolderSingleton {
private HolderSingleton() {}
private static class Holder {
private static final HolderSingleton INSTANCE = new HolderSingleton();
}
public static HolderSingleton getInstance() {
return Holder.INSTANCE;
}
}
enum EnumSingleton {
INSTANCE
}

这些方式主要保障实例创建和发布,不保证实例内所有可变业务状态线程安全。类加载器隔离也可能产生多个类身份,不能把单例理解为任何环境下全 JVM 绝对一个对象。Spring 的 singleton 则首先是容器中 Bean 定义的作用域,与手写单例模式不同。

面试回答模板#

JMM 定义线程间共享内存的可见性和排序规则。volatile 保证相关读写的可见性和顺序,但不保证 count++ 等复合操作原子。synchronized 使用同一对象监视器互斥,并建立解锁到后续加锁的 happens-before。简单状态发布可用 volatile,多步一致更新通常需要锁或合适的原子 API。

自测#

  1. volatile int 做 ++ 安全吗?不安全。
  2. 实例同步方法与静态同步方法一定使用同一把锁吗?不是。
  3. volatile List 引用能保证并发 add 安全吗?不能。
  4. 静态内部类单例为什么不需要手写 volatile?利用类初始化的同步和可见性保证。
JMM、volatile 与 synchronized:可见性、原子性和有序性
https://blog.xqcherry.top/posts/interview/java/jmm-volatile-synchronized/
作者
xqcherry
发布于
2026-10-12
许可协议
CC BY-NC-SA 4.0