必须用volatile修饰单例引用,因其能禁止指令重排序(防止“分配内存→赋值引用→构造函数”乱序)并保证可见性(确保其他线程读到完全初始化的对象),否则多线程下可能返回部分构造的实例。

为什么必须用 volatile 修饰单例引用
不加 volatile 的 getInstance() 在多线程下可能返回一个未完全构造的对象。根本原因是 instance = new Singleton() 不是原子操作,JVM 可能将其重排为「分配内存→赋值引用→执行构造函数」,导致另一个线程看到非 null 但字段全为默认值(如 0、null)的实例。
加上 volatile 后,它同时提供两层保障:
- 禁止指令重排序:确保构造函数执行完毕后,引用才对其他线程可见
- 保证可见性:任意线程读取
instance都能拿到最新写入的值,不会因 CPU 缓存不一致而读到旧值或部分初始化状态
synchronized(Singleton.class) 为什么不能换成 synchronized(this)
this 在实例未创建前根本不存在,所以无法作为锁对象;而 Singleton.class 是类加载时就存在的唯一对象,适合做全局同步锚点。
常见错误写法包括:
- 用
new Object()作为锁对象:每次新建对象,锁失效 - 用
instance自身做锁:首次调用时instance == null,会 NPE - 在静态方法里写
synchronized(this):编译不通过,静态上下文中无this
双重检查的两个 if (instance == null) 各自作用是什么
第一个 if 在同步块外,用于快速路:避免已初始化后每次调用都进锁,提升读性能。
第二个 if 在同步块内,用于防重入:防止多个线程排队进入同步块后,都执行 new Singleton()。
漏掉任一重检都会出问题:
- 只留外层
if:多个线程可能同时通过判断,一起进同步块,仍可能重复初始化 - 只留内层
if:每次调用都要抢锁,退化成低效的全同步方案
为什么不能把 volatile 换成 final 或 static final
final 字段只能在构造函数或声明时赋值,而懒汉式要求延迟到第一次调用才初始化,二者矛盾;static final 更不行——它强制类加载时就必须完成初始化,直接变成饿汉式。
另外注意:volatile 仅对引用本身起作用,不递归保护其内部字段。如果单例对象内部有可变状态,仍需额外同步机制。
真正容易被忽略的一点是:JDK 5+ 才完全支持 volatile 的禁止重排序语义。低于该版本的 JVM 中,即使写了 volatile,DCL 仍可能失效。

















