volatile在单例模式中确保new Singleton()三步(分配内存、初始化、赋值引用)不被重排序为①→③→②,防止线程看到未初始化完成的对象;同时保证可见性,使其他线程总能读到最新且完全初始化的instance。

volatile 在单例模式中不负责“线程安全”的全部,但它对保障初始化的有序性起决定性作用——没有它,双重检查锁定(DCL)就不可靠。
为什么有序性是关键问题
创建对象 new Singleton() 在 JVM 中实际分三步:
- 分配内存空间
- 调用构造方法初始化字段(比如加载配置、连接资源)
- 将
instance引用指向该内存地址(此时instance != null)
若 instance 没有 volatile 修饰,JVM 或 CPU 可能重排序为 ①→③→②。线程 A 执行到第③步后,instance 就已非 null;此时线程 B 进入外层 if (instance == null) 判断,直接返回该引用——但对象内部字段可能还是默认值(如 null、0),后续调用极易触发 NPE 或逻辑错误。
volatile 如何强制有序执行
volatile 通过插入内存屏障(Memory Barrier),禁止编译器和处理器对相关读写操作进行重排序。它确保:
立即学习“Java免费学习笔记(深入)”;
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 构造方法完成(步骤②)必须在引用赋值(步骤③)之前执行
- 其他线程看到的
instance非 null 时,一定对应一个完全初始化好的对象
这不是优化技巧,而是 DCL 正确性的必要条件。J2SE 5.0 之后才真正支持这种语义,此前的 DCL 实现存在严重隐患。
可见性同步同样不可少
即使顺序正确,若没有 volatile,线程仍可能因缓存不一致读到过期值:
- 线程 A 在 synchronized 块内完成
instance = new Singleton() - 线程 B 若未强制刷新工作内存,可能持续读到
null,反复进入同步块,浪费性能 - 更危险的是:B 某次读取恰好看到非 null 的
instance,却因未同步而拿到半初始化状态
volatile 保证每次读都从主内存加载最新值,每次写都立即刷回主内存,并使其他线程对应缓存失效,彻底消除这类不一致。
标准写法不能省略任何部分
以下是正确实现的核心结构:
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查(加锁后)
instance = new Singleton(); // volatile 确保这行的有序性+可见性
}
}
}
return instance;
}
两次判空缺一不可:第一次避免高并发下重复加锁;第二次防止多个线程同时通过第一次检查后争抢创建;而 volatile 是让这两层检查真正落地的底层保障。

















