单例模式有几种写法?
面试考察点
-
基础掌握度:面试官不仅仅是想知道你背了几种写法,更是想看你能否说出每种写法的优缺点和适用场景,而不是只会一种 "背答案"。
-
多线程安全意识:单例模式最核心的考点就是线程安全。面试官想看你是否理解并发环境下的问题,以及如何优雅地解决——这才是拉开差距的地方。
-
JVM 原理深度:能否讲清楚
volatile的作用、类加载机制、序列化破坏单例等底层原理,是区分 "会用" 和 "真正懂" 的关键。
核心答案
先说结论:单例模式主要有 5 种经典写法——
| 写法 | 线程安全 | 延迟加载 | 推荐指数 |
|---|---|---|---|
| 饿汉式 | ✅ 安全 | ❌ 否 | ⭐⭐⭐ |
| 懒汉式(同步方法) | ✅ 安全 | ✅ 是 | ⭐ |
| 双重检查锁(DCL) | ✅ 安全 | ✅ 是 | ⭐⭐⭐⭐ |
| 静态内部类 | ✅ 安全 | ✅ 是 | ⭐⭐⭐⭐⭐ |
| 枚举单例 | ✅ 安全 | ❌ 否 | ⭐⭐⭐⭐⭐ |
一句话总结:一般场景推荐静态内部类,需要防反射和序列化破坏时用枚举单例。
深度解析
一、饿汉式
类加载时就创建实例,简单粗暴,天生线程安全。
public class Singleton {
// 类加载时就初始化
private static final Singleton INSTANCE = new Singleton();
// 私有构造器,防止外部 new
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
优点是写法简单,线程安全(JVM 保证类加载过程的线程安全性)。缺点也很明显——不管用不用都先创建,浪费资源。如果你的单例对象很大但未必会用到,这就不合适了。
二、懒汉式(同步方法)
用到的时候才创建,但为了线程安全给整个方法加了 synchronized。
public class Singleton {
private static Singleton instance;
private Singleton() {}
// 整个方法加锁,性能堪忧
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
说实话,这种写法在面试里就是用来 "踩" 的。虽然线程安全,但每次调用 getInstance() 都要获取锁,而实际上只有第一次创建时才需要同步。这在高并发场景下性能损耗非常大,生产环境基本不用。
三、双重检查锁(DCL)
这个是面试的 "重头戏",也是面试官最爱追问的写法。
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?
这涉及到 JVM 的指令重排序问题。new Singleton() 这行代码在 JVM 层面并不是一个原子操作,它分三步:
上面的图展示了指令重排序导致的问题。具体来说:
- 正常顺序:① 分配内存 → ② 初始化对象 → ③ 指向引用。一切正常。
- 重排序后:① 分配内存 → ③ 指向引用 → ② 初始化对象。此时如果线程 A 执行到 ③,还没来得及执行 ②,线程 B 判断
instance != null,直接返回了一个半成品对象,使用时就会 NPE 或数据错乱。
加上 volatile 就是利用它的禁止指令重排序语义(内存屏障),保证 ①②③ 严格按照顺序执行。JDK 5 之后 volatile 才真正靠谱,所以 DCL 要求 JDK 5+。
四、静态内部类(推荐)
这是我个人最推荐的写法,优雅且无锁。
public class Singleton {
private Singleton() {}
// 静态内部类,外部类加载时不会立即加载
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
为什么说它好?利用了 JVM 的类加载机制来保证线程安全——外部类加载时并不会立即加载静态内部类,只有在调用 getInstance() 触发 Holder.INSTANCE 时,Holder 类才会被加载和初始化。而 JVM 在类加载初始化阶段会加锁,天然保证了线程安全,还实现了延迟加载。
无锁、线程安全、延迟加载、写法简洁,几乎没有缺点。唯一的问题是可以通过反射破坏单例。
五、枚举单例
Effective Java 作者 Josh Bloch 最推崇的写法。
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("单例方法");
}
}
// 使用
Singleton.INSTANCE.doSomething();
枚举单例号称 "最完美的单例",原因有三:
- 线程安全:枚举类的实例创建由 JVM 在类加载时保证
- 防反射:反射尝试通过
newInstance()创建枚举对象时,JVM 会直接抛异常 - 防序列化:枚举的序列化机制由 JVM 保证不会创建新对象
别的写法通过反射或序列化/反序列化都能破坏单例,只有枚举天然免疫。
深度补充:单例的 "破坏" 与 "防御"
面试官有时会追问:单例能被破坏吗? 答案是可以的,主要有两种攻击方式:
1. 反射破坏
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true); // 破坏 private 限制
Singleton broken = constructor.newInstance(); // 创建了第二个实例!
防御方式:在私有构造器中加判断——
private Singleton() {
if (Holder.INSTANCE != null) {
throw new RuntimeException("不允许反射创建实例");
}
}
2. 序列化/反序列化破坏
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.obj"));
oos.writeObject(instance);
oos.close();
// 反序列化 —— 产生新对象!
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.obj"));
Singleton broken = (Singleton) ois.readObject();
防御方式:实现 readResolve() 方法——
private Object readResolve() {
return getInstance(); // 返回已有实例,而不是创建新的
}
当然,用枚举单例就完全不需要操心这些。
面试高频追问
-
为什么 DCL 必须加
volatile?不加会怎样?- 防止指令重排序导致获取到未初始化的对象。不加的话在高并发下可能出现 NPE。
-
单例模式能被反射破坏吗?怎么防?
- 能。构造器里加判断,或者直接用枚举。
-
单例在分布式环境下还有效吗?
- 无效。单例只保证 JVM 进程内唯一,分布式环境下每个 JVM 都有自己的单例。分布式场景需要用分布式锁或集中式存储来保证。
-
Spring 的 Bean 默认是单例吗?和设计模式的单例一样吗?
- Spring Bean 默认
scope=singleton,但它是由 Spring 容器管理的单例,和传统单例模式不同。Spring 单例是每个容器内唯一,而且 Spring 处理了线程安全问题。
- Spring Bean 默认
常见面试变体
- "手写一个线程安全的单例"
- "单例模式怎么防止反射和序列化攻击?"
- "你项目中哪个地方用到了单例模式?"
- "Spring 的单例和设计模式的单例有什么区别?"
记忆口诀
五种写法:饿(饿汉)懒(懒汉)双(DCL)内(静态内部类)枚(枚举)——"饿懒双内枚"
DCL 三要素:两次检查一把锁,volatile 不能少。
推荐选择:一般用静态内部类,防攻击用枚举。
总结
面试答单例,先说五种写法的名称和特点,再重点展开 DCL 的 volatile 原理(指令重排序),最后提一嘴反射和序列化破坏以及枚举的完美防御。这条线拉下来,面试官基本满意。
