Skip to content

02 单例模式(Singleton)👑

目标:吃透单例模式的本质、常见实现方式、并发安全问题和面试高频问法。


1. 模式定义

单例模式:确保一个类在 JVM 中只有一个实例,并提供一个全局访问点。


2. 为什么不直接 new

你这个问题非常关键:很多对象确实可以直接 new,而且应该直接 new

只有当对象满足“全局唯一”约束时,单例才有价值。否则强行单例只会增加耦合。

直接 new 更好的场景:

  • 业务对象有状态(如 OrderUserSession
  • 每次请求都需要独立实例
  • 测试里需要灵活构造和替换对象

单例更有价值的场景:

  • 需要全局共享同一份资源(线程池、配置中心、连接池)
  • 创建成本高,重复创建浪费资源
  • 必须统一入口和统一行为

一句话判断:

如果“创建多个实例”不会出错,也不会浪费明显资源,就优先直接 new


3. 适用场景

单例适合“全局唯一、重复创建成本高、需要统一状态/入口”的对象,例如:

  • 配置中心对象(读取配置文件、环境变量)
  • 日志组件(统一写日志入口)
  • 线程池管理器(统一资源调度)
  • 数据库连接池管理器(统一连接策略)
  • Spring 中默认的单例 Bean(默认作用域)

不适合场景:

  • 对象有明显业务状态,且需要多实例隔离
  • 单元测试中需要频繁 mock / 重置实例

4. Java 实现

3.1 饿汉式

“饿汉”指的是类一加载就把实例创建好,不管后续有没有用到这个实例。

java
public class HungrySingleton {
    private static final HungrySingleton INSTANCE = new HungrySingleton();

    private HungrySingleton() {}

    public static HungrySingleton getInstance() {
        return INSTANCE;
    }
}

特点:

  • 优点:实现简单、天然线程安全
  • 缺点:即使不用也会初始化,可能浪费资源

3.2 懒汉式

“懒汉”指的是第一次真正使用时才创建实例,属于延迟加载思路。

java
public class LazySingletonUnsafe {
    private static LazySingletonUnsafe instance;

    private LazySingletonUnsafe() {}

    public static LazySingletonUnsafe getInstance() {
        if (instance == null) {
            instance = new LazySingletonUnsafe();
        }
        return instance;
    }
}

为什么线程不安全:

假设线程 A 和线程 B 同时第一次调用 getInstance()

  1. 线程 A 判断 instance == null,结果为 true
  2. 线程 B 也判断 instance == null,结果也为 true
  3. 两个线程都会执行 new LazySingletonUnsafe()
  4. 最终创建出两个不同对象,单例被破坏

结论:这种写法只适合单线程环境,在多线程环境下不可靠。

3.3 懒汉式 + synchronized

既然问题出在“多个线程同时进入创建逻辑”,最直接的修复方式就是加锁,让同一时刻只有一个线程能进入方法。

java
public class LazySingletonSync {
    private static LazySingletonSync instance;

    private LazySingletonSync() {}

    public static synchronized LazySingletonSync getInstance() {
        if (instance == null) {
            instance = new LazySingletonSync();
        }
        return instance;
    }
}

特点:线程安全,但每次获取实例都加锁,性能较低。

3.4 双重检查锁 DCL

为什么要 DCL:

  • synchronized 方法虽然安全,但每次调用都加锁,开销大
  • 实际上“只在第一次创建实例时需要加锁”,创建完后不该再加锁

DCL(Double-Checked Locking)思路:

  1. 第一次检查:如果实例已存在,直接返回,不加锁
  2. 进入同步块后第二次检查:防止多个线程排队进入时重复创建
  3. 只有两次都为 null 才真正 new 对象
java
public class DclSingleton {
    private static volatile DclSingleton instance;

    private DclSingleton() {}

    public static DclSingleton getInstance() {
        if (instance == null) {
            synchronized (DclSingleton.class) {
                if (instance == null) {
                    instance = new DclSingleton();
                }
            }
        }
        return instance;
    }
}

关键点:

  • 必须用 volatile,避免指令重排导致“拿到未完全初始化对象”。

为什么 volatile 必须有:

对象创建不是“一个原子动作”,可能被拆成三步:

  1. 分配内存
  2. 初始化对象
  3. 把内存地址赋值给 instance

如果发生指令重排,可能变成 1 -> 3 -> 2
这时另一个线程读到 instance != null,但对象其实还没初始化完成,就会出现隐患。
volatile 可以禁止这类重排,保证可见性与有序性。

3.5 静态内部类

这个写法的核心思想是:把实例创建动作放到内部类 Holder 里,Java 的类加载是按需触发(懒加载),不是一次把所有类都加载只有在第一次调用 getInstance() 时,Holder 才会被加载。

java
public class InnerClassSingleton {
    private InnerClassSingleton() {}

    private static class Holder {
        private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
    }

    public static InnerClassSingleton getInstance() {
        return Holder.INSTANCE;
    }
}

优势可以理解为三点:

  1. 延迟加载:第一次真正使用时才创建实例,不会像饿汉式那样提前占资源。
  2. 线程安全:依赖 JVM 类加载机制,类初始化过程天然线程安全,不需要手写 synchronized
  3. 性能更好:实例创建后,后续获取不需要加锁,调用开销低。

所以在工程里,它常被当作“实现成本低、综合表现好”的单例写法。

3.6 枚举单例

java
public enum EnumSingleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("Enum singleton working...");
    }
}

特点:天然防反射、防反序列化破坏,写法最简洁。


5. 优缺点(何时用、何时别用)

4.1 优点

  • 控制实例数量,避免资源重复占用
  • 提供全局统一访问点
  • 便于管理共享资源

4.2 缺点

  • 可能引入全局状态,增加耦合
  • 对测试不友好(难 mock)
  • 滥用会导致架构僵化

4.3 选型建议

  • 一般业务:优先 静态内部类枚举单例
  • 强防御需求:优先 枚举单例
  • 避免使用:线程不安全的懒汉式

6. 源码映射(JDK / Spring 对应应用点)

5.1 JDK 中的单例思想

  • Runtime.getRuntime():典型单例入口
  • Desktop.getDesktop():全局访问实例

5.2 Spring 中的单例思想

  • Spring Bean 默认作用域是 singleton
  • 容器启动时/首次使用时创建,之后从单例池复用
flowchart LR
    A["应用获取 Bean"] --> B["BeanFactory"]
    B --> C{"单例池是否存在"}
    C -- "是" --> D["直接返回同一实例"]
    C -- "否" --> E["创建 Bean 并放入单例池"]
    E --> D

7. 面试考点(高频问法与回答框架)

6.1 常见问题 1:单例有哪几种实现?推荐哪种?

回答框架:

  1. 先列举:饿汉、懒汉、同步懒汉、DCL、静态内部类、枚举
  2. 再比较线程安全与性能
  3. 给结论:常用 静态内部类,强安全选 枚举

6.2 常见问题 2:DCL 为什么要加 volatile?

回答关键词:

  • 防止指令重排
  • 避免返回“未初始化完成”的对象

6.3 常见问题 3:如何破坏单例?如何防御?

破坏方式:

  • 反射调用私有构造
  • 反序列化生成新对象

防御方式:

  • 构造器内做二次校验(有限)
  • 实现 readResolve()(序列化场景)
  • 使用枚举单例(最稳)

6.4 常见问题 4:Spring 单例和设计模式单例一样吗?

区别要点:

  • 设计模式单例:通常“一个 ClassLoader 一个实例”
  • Spring 单例:通常“一个 IoC 容器一个实例”

8. 小结

  • 单例模式核心不在“写出一个 getInstance()”,而在于并发安全和生命周期管理。
  • 面试最稳答案通常是:静态内部类 + 枚举单例 + DCL 原理说明
  • 工程中要克制使用单例,优先保证可测试性与低耦合。