两个 null 判断各做什么、发生在什么时候、去掉其中一个会怎样。
public final class Singleton {
private static volatile Singleton INSTANCE;
public static Singleton getInstance() {
if (INSTANCE != null) return INSTANCE; // ① 第一次检查(无锁,快路径)
synchronized (Singleton.class) {
if (INSTANCE == null) { // ② 第二次检查(有锁,竞态兜底)
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
}
① 外层 null 检查(第一次)
-
作用:在已完成初始化后,绝大多数调用可以直接返回,避免进入同步块,降低开销。
-
时机:每次 getInstance() 被调用时最先执行;初始化之后几乎所有请求都只走到这里就结束。
-
前提:INSTANCE 必须是 volatile,保证对其他线程立即可见且避免指令重排(否则可能看到“半初始化对象”)。
② 内层 null 检查(第二次)
- 作用:并发初始化的竞态兜底。当多个线程几乎同时通过了外层检查并竞争到锁时,只允许第一个线程真的创建实例;后续线程再次检查到 INSTANCE 已被设置,就不会再创建。
- 时机:只有在外层检查为 null 且成功拿到锁时才会执行。
去掉其中一个会怎样?
去掉外层检查(保留内层)
synchronized (Singleton.class) {
if (INSTANCE == null) INSTANCE = new Singleton();
}
return INSTANCE;
-
正确性:✅ 仍然线程安全,因为创建在同步块内且有二次检查。
-
性能:❌ 每次调用都加锁,哪怕已经初始化,多线程下开销明显。适合极少被调用的场景;一般不推荐。
去掉内层检查(只留外层)
if (INSTANCE == null) {
synchronized (Singleton.class) {
INSTANCE = new Singleton(); // ❌
}
}
-
正确性:❌ 不安全。两个线程可能都通过了外层检查:
T1 进锁创建完 → 释放锁;T2 随后获得锁再次创建,造成重复构造/覆盖,还可能泄漏第一次创建的对象。
-
总结:内层检查不能去。
额外提醒:不加 volatile 即使保留两次检查也不一定安全(可能读到“半初始化对象”)。DCL 想正确,volatile 必须加。
可替代且更简单的安全写法
- 静态内部类(按需加载) :
class Singleton {
private Singleton() {}
private static class Holder { static final Singleton I = new Singleton(); }
static Singleton getInstance() { return Holder.I; }
}
- 依赖类初始化的线程安全与延迟加载,代码更简洁,性能也很好。
- 枚举单例(若只需一个实例且可序列化):
enum Singleton { INSTANCE; }
一句话结论
- 外层检查=快路径,避免无谓加锁;内层检查=竞态兜底,保证只创建一次;volatile 必不可少。
- 去掉外层:功能对、性能差;去掉内层:功能错(会重复创建)。
- 若不追求 DCL,静态内部类是最稳妥、最简洁的工程选择。