Skip to content

单例模式

单例模式保证一个类在 JVM 中只有一个实例,并提供全局访问点。常见于配置对象、线程池、连接池、缓存管理器等场景。

为什么需要单例

有些对象不应该被反复创建:

  1. 创建成本高,例如连接池、线程池。
  2. 需要全局共享状态,例如配置中心客户端。
  3. 多个实例会造成资源冲突,例如本地缓存管理器。
mermaid
flowchart TD
    A["多个调用方"] --> B["统一访问 getInstance"]
    B --> C["同一个 Singleton 实例"]
    C --> D["共享配置或资源"]

如果本该全局唯一的对象被创建多个实例,可能出现:

场景后果
多个线程池线程数量失控
多个连接池数据库连接被耗尽
多个本地缓存数据不一致,内存浪费
多个配置管理器配置刷新状态混乱

饿汉式

类加载时就创建实例。

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

    private Singleton() {
    }

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

优点:

  1. 写法简单。
  2. 类加载机制保证线程安全。

缺点:

  1. 类加载时就创建,不能懒加载。
  2. 如果对象很重,但可能不会被使用,会浪费资源。

懒汉式双重检查

对象第一次使用时创建。

java
public class Singleton {
    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;
    }
}

创建流程:

mermaid
flowchart TD
    A["调用 getInstance"] --> B{"instance 是否为空"}
    B -- "否" --> C["直接返回"]
    B -- "是" --> D["进入 synchronized"]
    D --> E{"再次检查 instance"}
    E -- "为空" --> F["创建对象"]
    E -- "不为空" --> C
    F --> C

为什么要检查两次:

  1. 第一次检查:实例已经存在时不进入锁,提高性能。
  2. 第二次检查:多个线程排队进入锁后,防止重复创建。

为什么必须用 volatile

text
instance = new Singleton()

底层可以拆成:

  1. 分配内存。
  2. 调用构造方法初始化对象。
  3. 把引用赋值给 instance

如果没有 volatile,步骤 2 和 3 可能发生重排序。另一个线程可能看到 instance != null,但对象还没初始化完成。

静态内部类

推荐写法之一,既懒加载,又线程安全。

java
public class Singleton {
    private Singleton() {
    }

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

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

原理:外部类加载时不会立刻加载内部类,只有调用 getInstance() 时才加载 Holder,JVM 类加载机制保证初始化线程安全。

枚举单例

java
public enum Singleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("do something");
    }
}

枚举单例的优势:

  1. 写法简单。
  2. 天然防止反射破坏。
  3. 天然处理序列化问题。

如果不是需要继承类,枚举单例是很稳妥的写法。

Spring Bean 和单例模式

Spring Bean 默认是单例,但它是“Spring 容器级单例”,不是手写设计模式里的 JVM 全局单例。

对比手写单例Spring 单例 Bean
生命周期自己控制Spring 容器控制
创建方式getInstance()依赖注入
测试替换较困难可以 Mock 或替换 Bean
推荐程度特殊场景使用Spring 项目更常用

在 Spring 项目里,业务 Service、Repository、Client 通常交给容器管理,不需要手写单例。

滥用风险

风险后果
单例保存可变状态多线程并发读写出错
全局访问到处调用依赖关系隐藏,测试困难
构造复杂依赖初始化顺序难控制
和 Spring 容器混用生命周期混乱

错误示例:

java
public class UserContextHolder {
    private static final UserContextHolder INSTANCE = new UserContextHolder();
    private Long currentUserId;
}

currentUserId 是请求级状态,不应该放在全局单例里,否则不同用户请求会互相污染。

小结

单例模式适合全局唯一、创建成本高、需要统一管理的对象。它的重点不是“少创建对象”,而是“控制共享资源的唯一入口”。在 Spring 项目中,优先使用容器管理单例;只有容器外工具类或特殊资源管理器才考虑手写单例。