Skip to content

单例模式 (Singleton Pattern)

一、定义

一句话概括:确保一个类只有一个实例,并提供一个全局访问点来获取该实例。

官方定义(GoF):Ensure a class has only one instance, and provide a global point of access to it.

单例模式是创建型设计模式中最简单、也是最常被面试问到的一种模式。它的核心思想是:将类的构造方法私有化,由类自身负责创建唯一实例,并通过静态方法对外暴露。


二、解决的问题

2.1 什么场景下需要单例模式?

  • 资源控制:某些对象创建和销毁代价高昂(如数据库连接池、线程池),需要复用。
  • 全局状态管理:系统中需要唯一的配置管理器、缓存管理器、日志记录器等。
  • 协调访问:多个模块需要共享同一个状态或数据源,如全局计数器、应用上下文。
  • I/O 设备访问:打印机、文件系统等物理资源只能有一个操作入口。

2.2 不用单例模式会有什么问题?

假设没有单例模式,每次需要数据库连接时都 new 一个新的连接对象:

问题1:内存浪费 —— 每个请求都创建新连接,GC 压力大
问题2:连接数耗尽 —— 数据库连接数有限,频繁创建会耗尽连接池
问题3:状态不一致 —— 多个实例各自维护缓存,数据不同步
问题4:难以管理 —— 无法统一控制资源的生命周期

三、结构

3.1 文字描述

单例模式结构非常简单,只包含一个类:

  • Singleton(单例类):包含一个私有静态变量持有自身实例,一个私有构造方法,以及一个公开静态方法返回唯一实例。

3.2 ASCII 类图

┌─────────────────────────────┐
│         Singleton            │
├─────────────────────────────┤
│ - static instance: Singleton │  ← 私有静态实例
│ - Singleton()                │  ← 私有构造器
│ + static getInstance()       │  ← 全局访问点
├─────────────────────────────┤
│  ... 其他业务方法             │
└─────────────────────────────┘

3.3 时序图

Client                Singleton
  │                       │
  │── getInstance() ─────>│
  │                       │── instance == null ?
  │                       │   ├── YES: new Singleton()
  │                       │   └── NO:  return instance
  │<── instance ──────────│
  │                       │
  │── doSomething() ─────>│
  │<── result ────────────│

四、代码实现

4.1 基础实现

4.1.1 饿汉式(Eager Initialization)

最简单的实现,类加载时就创建实例,线程安全但可能浪费内存。

java
/**
 * 饿汉式单例 —— 类加载时立即初始化
 * 优点:线程安全,实现简单
 * 缺点:如果实例从未被使用,造成内存浪费
 */
public class EagerSingleton {

    // 类加载时就创建实例(static final 保证线程安全)
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    // 私有构造器,防止外部 new
    private EagerSingleton() {
        System.out.println("EagerSingleton 实例已创建");
        // 防止反射攻击(可选)
        if (INSTANCE != null) {
            throw new RuntimeException("单例已存在,不允许反射创建");
        }
    }

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }

    // 业务方法
    public void doSomething() {
        System.out.println("EagerSingleton 执行业务逻辑: " + this.hashCode());
    }
}

4.1.2 懒汉式(Lazy Initialization)

延迟加载,但存在线程安全问题。

java
/**
 * 懒汉式单例(线程不安全版本)
 * 问题:多线程并发时可能创建多个实例
 */
public class LazySingletonUnsafe {

    private static LazySingletonUnsafe instance;

    private LazySingletonUnsafe() {
        System.out.println("LazySingletonUnsafe 实例已创建");
    }

    // 线程不安全!多线程可能同时进入 if 块
    public static LazySingletonUnsafe getInstance() {
        if (instance == null) {
            instance = new LazySingletonUnsafe();
        }
        return instance;
    }
}

4.1.3 懒汉式(同步方法版本)

java
/**
 * 懒汉式单例(同步方法版本)
 * 问题:synchronized 加在方法上,性能差,每次获取都要加锁
 */
public class LazySingletonSync {

    private static LazySingletonSync instance;

    private LazySingletonSync() {
        System.out.println("LazySingletonSync 实例已创建");
    }

    // 整个方法加锁,并发性能极差
    public static synchronized LazySingletonSync getInstance() {
        if (instance == null) {
            instance = new LazySingletonSync();
        }
        return instance;
    }
}

4.2 进阶实现

4.2.1 双重检查锁定(Double-Checked Locking, DCL)

这是面试中最常被问到的实现方式,也是生产环境常用方案。

java
/**
 * 双重检查锁定(DCL)单例
 * 核心要点:
 * 1. volatile 防止指令重排序(禁止 new 的 3 步操作被重排)
 * 2. 两次 null 检查,减少不必要的同步开销
 * 3. synchronized 只锁关键代码块
 */
public class DCLSingleton {

    /**
     * volatile 的作用:
     * - new 对象不是原子操作,分三步:
     *   1. 分配内存空间
     *   2. 调用构造器初始化对象
     *   3. 将 instance 引用指向内存地址
     * - JVM 可能重排序为 1→3→2,此时 instance 不为 null 但尚未初始化完成
     * - volatile 禁止这种重排序,保证可见性和有序性
     */
    private static volatile DCLSingleton instance;

    private DCLSingleton() {
        System.out.println("DCLSingleton 实例已创建");
        // 防止反射破坏
        if (instance != null) {
            throw new RuntimeException("单例已存在,禁止反射创建");
        }
    }

    public static DCLSingleton getInstance() {
        // 第一次检查:避免不必要的同步
        if (instance == null) {
            // 只锁关键代码块,减小锁粒度
            synchronized (DCLSingleton.class) {
                // 第二次检查:防止多个线程同时通过第一次检查
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }

    public void doSomething() {
        System.out.println("DCLSingleton 执行: " + this.hashCode());
    }
}

4.2.2 静态内部类(推荐方式)

利用 JVM 类加载机制保证线程安全,实现懒加载。这是《Effective Java》推荐的方式。

java
/**
 * 静态内部类实现单例(推荐方式)
 *
 * 原理:
 * - JVM 在加载外部类时不会加载内部类,只有调用 getInstance() 时才加载
 * - 类加载过程是线程安全的(由 JVM 保证)
 * - 不需要 volatile 和 synchronized,代码简洁
 */
public class InnerClassSingleton {

    private InnerClassSingleton() {
        System.out.println("InnerClassSingleton 实例已创建");
        // 防止反射
        if (SingletonHolder.INSTANCE != null) {
            throw new RuntimeException("单例已存在");
        }
    }

    /**
     * 静态内部类:只有在被引用时才会被 JVM 加载
     * 类的初始化阶段(<clinit>)是线程安全的
     */
    private static class SingletonHolder {
        private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
    }

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

    public void doSomething() {
        System.out.println("InnerClassSingleton 执行: " + this.hashCode());
    }
}

4.2.3 枚举实现(最安全的方式)

《Effective Java》作者 Joshua Bloch 强烈推荐的方式,能天然防御反射和序列化攻击。

java
/**
 * 枚举实现单例 —— 最简单、最安全的方式
 *
 * 优点:
 * 1. 写法极其简单
 * 2. 天然线程安全(JVM 保证枚举实例的唯一性)
 * 3. 天然防止反射攻击(JDK 内部禁止反射创建枚举实例)
 * 4. 天然防止序列化破坏(反序列化不会创建新实例)
 * 5. 可以包含字段和方法,功能完整
 */
public enum EnumSingleton {

    INSTANCE;

    // 枚举可以有自己的字段
    private String configValue = "default";

    // 枚举可以有构造器(默认 private)
    EnumSingleton() {
        System.out.println("EnumSingleton 实例已创建");
    }

    // 枚举可以有方法
    public void doSomething() {
        System.out.println("EnumSingleton 执行: " + this.hashCode());
    }

    public String getConfigValue() {
        return configValue;
    }

    public void setConfigValue(String configValue) {
        this.configValue = configValue;
    }
}

4.2.4 反序列化攻击与防御

普通单例在反序列化时会创建新实例,破坏单例约束。

java
import java.io.*;

/**
 * 演示反序列化如何破坏单例,以及如何防御
 */
public class SerializableSingleton implements Serializable {

    private static final long serialVersionUID = 1L;

    private static final SerializableSingleton INSTANCE = new SerializableSingleton();

    private SerializableSingleton() {
        System.out.println("SerializableSingleton 构造器被调用");
    }

    public static SerializableSingleton getInstance() {
        return INSTANCE;
    }

    /**
     * 防御反序列化攻击的关键方法!
     * ObjectInputStream 在反序列化时会检查该类是否定义了 readResolve()
     * 如果有,则用 readResolve() 的返回值替换反序列化出来的对象
     * 从而保证返回的是同一个实例
     */
    private Object readResolve() {
        System.out.println("readResolve 被调用,返回已有实例");
        return INSTANCE;
    }

    // ========== 测试代码 ==========

    public static void main(String[] args) throws Exception {
        SerializableSingleton original = SerializableSingleton.getInstance();

        // 序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(original);
        oos.close();

        // 反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        SerializableSingleton deserialized = (SerializableSingleton) ois.readObject();
        ois.close();

        // 验证:有 readResolve() 时,两个引用指向同一个对象
        System.out.println("original    == deserialized ? " + (original == deserialized));
        System.out.println("original.hashCode()    = " + original.hashCode());
        System.out.println("deserialized.hashCode() = " + deserialized.hashCode());
    }
}

4.2.5 反射攻击与防御

java
import java.lang.reflect.Constructor;

/**
 * 演示反射如何破坏单例(除枚举外),以及如何防御
 */
public class ReflectionSafeSingleton {

    // 使用一个标志位标记是否已创建实例
    private static volatile boolean isCreated = false;

    private static final ReflectionSafeSingleton INSTANCE = new ReflectionSafeSingleton();

    private ReflectionSafeSingleton() {
        synchronized (ReflectionSafeSingleton.class) {
            if (isCreated) {
                throw new RuntimeException("单例模式已被破坏!拒绝通过反射创建新实例");
            }
            isCreated = true;
        }
        System.out.println("ReflectionSafeSingleton 构造器被调用");
    }

    public static ReflectionSafeSingleton getInstance() {
        return INSTANCE;
    }

    // ========== 测试反射攻击 ==========

    public static void main(String[] args) throws Exception {
        // 正常获取
        ReflectionSafeSingleton instance1 = ReflectionSafeSingleton.getInstance();
        System.out.println("正常获取: " + instance1.hashCode());

        // 尝试反射攻击
        try {
            Constructor<ReflectionSafeSingleton> constructor =
                    ReflectionSafeSingleton.class.getDeclaredConstructor();
            constructor.setAccessible(true); // 绕过 private 限制
            ReflectionSafeSingleton instance2 = constructor.newInstance();
            System.out.println("反射创建: " + instance2.hashCode());
        } catch (Exception e) {
            System.out.println("反射攻击被阻止: " + e.getCause().getMessage());
        }
    }
}

4.3 生产级实现(Spring Boot 场景)

在实际的 Spring Boot 项目中,Spring 容器管理的 Bean 默认就是单例的。但有时我们需要在非 Spring 管理的类中实现单例,或者需要自定义单例行为。

java
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 生产级应用配置管理器 —— 结合 Spring Boot 的单例实现
 *
 * 业务场景:全局唯一配置管理器,负责加载和缓存应用配置
 */
@Configuration
public class AppConfigManager {

    // ========== 内部单例类(使用静态内部类) ==========

    /**
     * 全局配置缓存,使用静态内部类实现单例
     * 负责从数据库/配置中心加载配置并缓存
     */
    public static class ConfigCache {

        private final AtomicLong lastRefreshTime = new AtomicLong(System.currentTimeMillis());

        private ConfigCache() {
            // 模拟从配置中心加载配置
            System.out.println("[ConfigCache] 初始化,加载配置文件...");
            loadConfigurations();
        }

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

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

        public void loadConfigurations() {
            lastRefreshTime.set(System.currentTimeMillis());
            // 实际项目中:从 Apollo / Nacos / Consul 等配置中心拉取
            System.out.println("[ConfigCache] 配置已刷新");
        }

        public long getLastRefreshTime() {
            return lastRefreshTime.get();
        }

        public String getConfig(String key) {
            // 模拟返回配置值
            return "value_of_" + key;
        }
    }

    // ========== Spring 管理的单例 Bean ==========

    /**
     * 默认情况下 Spring Bean 就是单例的
     * 显式声明 scope 为 singleton 只是为了代码可读性
     */
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    public ConfigCache configCache() {
        return ConfigCache.getInstance();
    }
}

/**
 * 生产级线程安全的单例 —— 数据库连接池管理器
 *
 * 场景:应用中只需要一个连接池管理器,负责管理所有数据库连接
 */
class ConnectionPoolManager {

    private static volatile ConnectionPoolManager instance;

    // 连接池相关字段
    private final int maxConnections;
    private final int activeConnections;

    private ConnectionPoolManager() {
        // 从配置文件读取连接池参数
        this.maxConnections = 20;
        this.activeConnections = 0;
        System.out.println("[ConnectionPoolManager] 连接池初始化完成,最大连接数: " + maxConnections);
    }

    // 双重检查锁定
    public static ConnectionPoolManager getInstance() {
        if (instance == null) {
            synchronized (ConnectionPoolManager.class) {
                if (instance == null) {
                    instance = new ConnectionPoolManager();
                }
            }
        }
        return instance;
    }

    public int getMaxConnections() {
        return maxConnections;
    }

    public int getActiveConnections() {
        return activeConnections;
    }
}

/**
 * 生产级 ThreadLocal 单例 —— 每线程单例
 *
 * 场景:数据库事务管理器,每个线程需要一个独立的数据库连接
 * 注意:这不是严格意义上的单例,而是"每线程单例"的变体
 */
class ThreadLocalSingleton {

    private static final ThreadLocal<ThreadLocalSingleton> threadLocal =
            ThreadLocal.withInitial(ThreadLocalSingleton::new);

    private ThreadLocalSingleton() {
        System.out.println("[Thread-" + Thread.currentThread().getId() + "] ThreadLocalSingleton 创建");
    }

    public static ThreadLocalSingleton getInstance() {
        return threadLocal.get();
    }

    public void remove() {
        threadLocal.remove();
    }
}

五、优缺点

优点

优点说明
全局唯一实例严格控制实例数量,节省系统资源
全局访问点提供一个统一的访问入口,方便管理和维护
延迟加载懒汉式实现可以在使用时才创建实例,节省启动时间
避免资源冲突对于需要互斥访问的资源,单例天然保证互斥

缺点

缺点说明
违反单一职责原则单例类既要管理自己的业务逻辑,又要管理自己的创建
扩展困难单例类没有抽象层,不利于扩展
隐藏依赖关系调用方直接使用 getInstance(),依赖关系不透明
不利于单元测试单例的状态是全局的,测试之间会相互影响
可能产生内存泄漏单例生命周期与 JVM 一致,持有的大对象无法被 GC

六、适用场景

  1. 需要频繁创建和销毁的对象:如数据库连接池、线程池
  2. 创建开销大的对象:如 Hibernate SessionFactory
  3. 需要全局共享的配置对象:如系统配置、环境变量管理
  4. 日志记录器:通常一个应用只需要一个日志实例
  5. 设备管理器:如打印机管理器、文件系统管理器
  6. 缓存管理器:全局缓存如 Redis 连接管理
  7. 应用上下文:如 Spring ApplicationContext
  8. 计数器/序列号生成器:需要全局唯一的序列号

七、JDK / Spring 框架中的实际应用

7.1 JDK 中的单例

1. java.lang.Runtime
   - Runtime.getRuntime() 返回 JVM 唯一的 Runtime 实例
   - 用于执行系统命令、获取内存信息等

2. java.awt.Desktop
   - Desktop.getDesktop() 返回当前桌面环境的实例

3. java.lang.System
   - 虽然 System 是工具类(全部静态方法),但其内部初始化使用了单例思想

4. java.util.logging.LogManager
   - LogManager.getLogManager() 返回全局唯一的 LogManager

7.2 Spring 框架中的单例

1. Spring Bean 默认作用域是 singleton
   - 所有通过 @Bean、@Component、@Service 等注解声明的 Bean 默认都是单例
   - 由 Spring IoC 容器统一管理生命周期

2. ApplicationContext
   - 每个 Spring 容器只有一个 ApplicationContext 实例

3. DefaultListableBeanFactory
   - Spring 内部的 Bean 工厂实现,全局唯一

4. Environment
   - Spring 环境配置对象,通常是单例

5. AbstractAutowireCapableBeanFactory
   - Spring 底层 Bean 工厂核心类,单例

7.3 源码示例:Spring 单例 Bean 注册

java
// Spring 源码中 DefaultSingletonBeanRegistry 核心逻辑(简化)
public class DefaultSingletonBeanRegistry {
    // 一级缓存:存储已经完全创建好的单例 Bean
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    // 二级缓存:存储早期暴露的 Bean(解决循环依赖)
    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

    // 三级缓存:存储 Bean 的 ObjectFactory
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

    public Object getSingleton(String beanName) {
        // 先从一级缓存获取
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null) {
            // 从二级缓存获取
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null) {
                // 从三级缓存获取 ObjectFactory 并创建
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
        return singletonObject;
    }
}

八、与其他模式的关系

相关模式关系说明
工厂方法模式工厂类本身常被设计为单例,避免重复创建工厂对象
抽象工厂模式同工厂方法,具体工厂类常设计为单例
建造者模式建造者通常不是单例,但建造的产品可以是单例
原型模式与单例相反:原型模式通过克隆创建多个实例,单例则限制为唯一实例
享元模式两者都涉及共享实例,但享元可能有多个不同状态的实例,单例只有一个
外观模式外观类经常被设计为单例,因为只需要一个外观对象

九、面试常见问题

Q1:双重检查锁定(DCL)中为什么要用 volatile?

答案new Singleton() 并非原子操作,JVM 将其分为三步:

  1. 分配内存空间
  2. 调用构造器初始化对象
  3. 将引用指向内存地址

JVM 可能将步骤 2 和 3 重排序(指令重排),导致 1→3→2 的执行顺序。此时如果另一个线程在步骤 2 完成前读取 instance,会发现 instance != null 但对象尚未初始化完成,从而访问到未初始化的对象。

volatile 关键字禁止了这种指令重排序,确保 instance 引用指向的一定是已完全初始化的对象。


Q2:为什么推荐使用枚举实现单例?

答案

  1. 线程安全:JVM 底层保证枚举实例的创建是线程安全的
  2. 防止反射攻击Constructor.newInstance() 在 JDK 内部对枚举类型做了特殊判断,会抛出 IllegalArgumentException
  3. 防止序列化破坏:枚举的序列化机制由 JVM 保证,反序列化不会创建新实例
  4. 代码简洁:一行 INSTANCE; 即可完成,无需手写复杂逻辑
  5. 功能完整:枚举可以有字段、方法和构造器,与普通类无异

Q3:静态内部类和 DCL 各有什么优缺点?

答案

对比维度静态内部类DCL
简洁性更简洁,无需 volatile/synchronized代码较复杂
性能利用 JVM 类加载机制,无锁开销首次创建时有锁,后续无锁
安全性天然线程安全依赖 volatile 的正确使用
灵活性无法传递参数可在 getInstance() 中传递参数
异常处理构造器异常会导致 NoClassDefFoundError可以正常捕获构造器异常

Q4:如何破坏单例模式?如何防御?

答案

破坏方式

  1. 反射:通过 setAccessible(true) 调用私有构造器
  2. 序列化/反序列化:将单例对象序列化后再反序列化,会创建新对象
  3. 克隆:如果实现了 Cloneable 接口,clone() 会创建新对象
  4. 多 ClassLoader:不同类加载器加载同一个类会产生多个实例

防御方式

  1. 反射:在构造器中判断实例是否已存在,已存在则抛异常
  2. 序列化:添加 readResolve() 方法返回已有实例
  3. 克隆:重写 clone() 方法返回已有实例,或直接抛异常
  4. 多 ClassLoader:指定同一个 ClassLoader 加载,或使用枚举(天然解决)

Q5:Spring 中的单例 Bean 和 GoF 单例模式有什么区别?

答案

对比维度Spring 单例 BeanGoF 单例模式
范围单例范围是 Spring 容器级别单例范围是 JVM 级别(ClassLoader 级别)
实现方式由 IoC 容器管理,通过 ConcurrentHashMap 缓存在类内部通过静态变量实现
灵活性可通过 @Scope 切换为 prototype/request/session固定为单例,无法切换
依赖注入支持依赖注入和 AOP 代理不支持
生命周期由容器管理,支持 InitializingBean/DisposableBean与 JVM 生命周期一致

核心区别:Spring 的单例是"容器内单例",即同一个 Bean 名称在同一个容器中只有一个实例;而 GoF 单例是"类级别单例",即整个 JVM 中该类只有一个实例。如果有两个 Spring 容器,就会有两个"单例 Bean"。


总结

单例模式是面试最高频的设计模式,其核心考点包括:

  1. volatile 在 DCL 中的作用(指令重排序 + 可见性)
  2. 枚举实现单例的优势(天然防反射、防序列化)
  3. 静态内部类的实现原理(JVM 类加载机制)
  4. 破坏单例的方式与防御手段
  5. Spring 单例 Bean 的本质(容器级单例 vs 类级单例)

实际开发中建议:

  • 简单场景:使用枚举
  • 需要懒加载 + 灵活性:使用静态内部类
  • 需要传递参数构造:使用DCL
  • 在 Spring 项目中:直接使用 Spring 的单例 Bean,无需手动实现