面试知识库
极高 进阶

单例模式#

一句话答案#

保证类只有一个实例,推荐枚举实现(最安全)或双重检查锁 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)

DCL 为什么必须加 volatile?

INSTANCE = new Singleton() 不是原子操作,实际分三步:

  1. 分配内存空间
  2. 初始化对象(调用构造方法)
  3. 将引用指向分配的内存

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 之前确实有问题(指令重排)