实现线程安全的单例模式,面试官通常考察的是你对 指令重排序、原子性、以及 JVM 加载机制 的理解。
最推荐的两种写法是 “双重检查锁(DCL)” 和 “静态内部类”。
1. 双重检查锁 (Double-Check Locking, DCL)
这是面试中最常写的版本,因为它能体现你对 volatile 和锁性能优化的理解。
public class Singleton {
// 必须加 volatile,防止指令重排序
private static volatile Singleton instance;
private Singleton() {} // 私有构造防止外部实例化
public static Singleton getInstance() {
if (instance == null) { // 第一次检查:避免不必要的锁竞争
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查:确定真的没被创建
instance = new Singleton();
}
}
}
return instance;
}
}
面试官必问:为什么要加 volatile?
-
关键点:
instance = new Singleton();并不是一个原子操作。它分为三步:-
分配内存空间。
-
初始化对象。
-
将
instance指向分配的地址。
-
-
如果没有
volatile,CPU 可能会为了优化执行 1 -> 3 -> 2。 -
如果线程 A 执行到 3 但还没执行 2,线程 B 进来判断
instance != null,直接拿走了一个还没初始化的半成品对象,导致空指针异常。
2. 静态内部类 (Static Inner Class) —— 最推荐
这种方式既实现了懒加载(Lazy Loading),又保证了绝对的线程安全,代码还简洁。
public class Singleton {
private Singleton() {}
// 静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
原理:
-
线程安全: 虚拟机会保证一个类的
<clinit>()方法在多线程环境中被正确地加锁、同步。 -
懒加载: 只有在调用
getInstance()时,才会加载SingletonHolder类,此时才实例化对象,不占用多余内存。
3. 枚举单例 (Enum) —— 最安全
如果你想给面试官一个“标准答案之外”的惊喜,可以提这个。
public enum Singleton {
INSTANCE;
public void doSomething() { }
}
优点:
-
防反射/防序列化: 前面两种方法都可以通过反射暴力破解(拿到私有构造函数),或者通过序列化破坏单例。枚举是 JVM 底层保证的,唯一能防止这些手段的单例实现。
总结建议
-
口播面试: 先说双重检查锁,然后主动解释
volatile的作用(显得你懂底层)。 -
笔试/写代码: 写静态内部类版本,显得你代码风格稳健且追求优雅。
-
炫技点: 提一句“如果涉及反序列化,枚举单例是最安全的”。