单例模式#
一句话答案#
保证类只有一个实例,推荐枚举实现(最安全)或双重检查锁 DCL(volatile 禁止重排防止获取未初始化对象)。
核心要点
单例模式确保一个类只有一个实例,并提供全局访问点。常见写法有四种:
1. 饿汉式(推荐用于简单场景)
public class Singleton {
// 类加载时即初始化,JVM 保证线程安全
private static final Singleton INSTANCE = new Singleton();
private Singleton() {} // 私有构造
public static Singleton getInstance() {
return INSTANCE;
}
}java- 优点:实现简单,线程安全(JVM 保证 static 变量只初始化一次)
- 缺点:类加载就实例化,不支持懒加载
2. 懒汉式 DCL(Double-Checked Locking)
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;
}
}javaDCL 为什么必须加 volatile?
INSTANCE = new Singleton() 不是原子操作,实际分三步:
- 分配内存空间
- 初始化对象(调用构造方法)
- 将引用指向分配的内存
JVM 可能对步骤 2 和 3 进行指令重排序,变成 1→3→2。如果线程 A 执行了 1→3 但还未执行 2,此时线程 B 在第一次检查时发现 INSTANCE != null,直接返回了一个未完成初始化的对象,导致 NPE 或数据异常。
volatile 通过内存屏障禁止指令重排序,保证 1→2→3 的顺序执行。→ 详见 JMM与happens-before
3. 静态内部类(推荐)
public class Singleton {
private Singleton() {}
// 静态内部类在外部类加载时不会被加载,实现懒加载
// 内部类加载时 JVM 保证线程安全
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}java- 兼具懒加载 + 线程安全,无锁性能好
4. 枚举(终极写法,Effective Java 推荐)
public enum Singleton {
INSTANCE;
public void doSomething() {
// 业务方法
}
}java- JVM 保证唯一实例
- 天然防止反射攻击和反序列化破坏单例
- 缺点:不支持懒加载,写法不够直观
对比总结:
| 写法 | 线程安全 | 懒加载 | 防反射 | 推荐度 |
|---|---|---|---|---|
| 饿汉式 | 是 | 否 | 否 | ★★★ |
| DCL | 是(需 volatile) | 是 | 否 | ★★★★ |
| 静态内部类 | 是 | 是 | 否 | ★★★★★ |
| 枚举 | 是 | 否 | 是 | ★★★★★ |
面试回答(2分钟版)
单例模式保证一个类全局只有一个实例。常见四种写法各有特点。饿汉式最简单,利用static final在类加载时就创建实例,JVM保证线程安全,缺点是不支持懒加载。DCL双重检查锁是面试重点,外层if判空避免不必要的加锁,synchronized块内再判一次防止重复创建,关键是实例变量必须加volatile。原因是new操作分三步——分配内存、初始化对象、引用指向内存,JVM可能把第二步和第三步重排序,导致其他线程拿到还没初始化完的对象,volatile通过内存屏障禁止这个重排。静态内部类兼具懒加载和线程安全,外部类加载时内部类不会加载,调用getInstance时才触发内部类加载和实例化,JVM的类加载机制保证线程安全且无锁。枚举是Effective Java推荐的终极写法,JVM保证实例唯一,天然防止反射攻击和反序列化破坏单例,缺点是不支持懒加载。Spring的单例和设计模式的单例不同,Spring是容器级别的单例而非类级别的。
追问与易错
追问方向:
- “DCL 为什么需要 volatile?”→ new 操作分三步(分配内存/初始化/引用赋值),JVM 可能重排为 1→3→2,其他线程拿到未初始化的对象导致 NPE;volatile 通过内存屏障禁止重排
- “枚举单例为什么最安全?”→ JVM 保证枚举实例唯一、天然防止反射攻击(Constructor.newInstance 会检查 enum 类型直接抛异常)、天然防止反序列化破坏单例(readResolve 返回已有实例)
- “Spring 的单例和设计模式的单例有什么区别?”→ 设计模式的单例是类级别的,通过私有构造函数保证全 JVM 唯一;Spring 的单例是容器级别的,同一个 BeanName 在同一个容器中唯一,但不同容器可以有不同实例
易错点:
- ❌ “饿汉式有性能问题”——类不使用不会加载,实际很少浪费
- ❌ “DCL 不加 volatile 也行”——JDK5 之前确实有问题(指令重排)