Appearance
单例模式 (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 |
六、适用场景
- 需要频繁创建和销毁的对象:如数据库连接池、线程池
- 创建开销大的对象:如 Hibernate SessionFactory
- 需要全局共享的配置对象:如系统配置、环境变量管理
- 日志记录器:通常一个应用只需要一个日志实例
- 设备管理器:如打印机管理器、文件系统管理器
- 缓存管理器:全局缓存如 Redis 连接管理
- 应用上下文:如 Spring ApplicationContext
- 计数器/序列号生成器:需要全局唯一的序列号
七、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() 返回全局唯一的 LogManager7.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 将其分为三步:
- 分配内存空间
- 调用构造器初始化对象
- 将引用指向内存地址
JVM 可能将步骤 2 和 3 重排序(指令重排),导致 1→3→2 的执行顺序。此时如果另一个线程在步骤 2 完成前读取 instance,会发现 instance != null 但对象尚未初始化完成,从而访问到未初始化的对象。
volatile 关键字禁止了这种指令重排序,确保 instance 引用指向的一定是已完全初始化的对象。
Q2:为什么推荐使用枚举实现单例?
答案:
- 线程安全:JVM 底层保证枚举实例的创建是线程安全的
- 防止反射攻击:
Constructor.newInstance()在 JDK 内部对枚举类型做了特殊判断,会抛出IllegalArgumentException - 防止序列化破坏:枚举的序列化机制由 JVM 保证,反序列化不会创建新实例
- 代码简洁:一行
INSTANCE;即可完成,无需手写复杂逻辑 - 功能完整:枚举可以有字段、方法和构造器,与普通类无异
Q3:静态内部类和 DCL 各有什么优缺点?
答案:
| 对比维度 | 静态内部类 | DCL |
|---|---|---|
| 简洁性 | 更简洁,无需 volatile/synchronized | 代码较复杂 |
| 性能 | 利用 JVM 类加载机制,无锁开销 | 首次创建时有锁,后续无锁 |
| 安全性 | 天然线程安全 | 依赖 volatile 的正确使用 |
| 灵活性 | 无法传递参数 | 可在 getInstance() 中传递参数 |
| 异常处理 | 构造器异常会导致 NoClassDefFoundError | 可以正常捕获构造器异常 |
Q4:如何破坏单例模式?如何防御?
答案:
破坏方式:
- 反射:通过
setAccessible(true)调用私有构造器 - 序列化/反序列化:将单例对象序列化后再反序列化,会创建新对象
- 克隆:如果实现了 Cloneable 接口,
clone()会创建新对象 - 多 ClassLoader:不同类加载器加载同一个类会产生多个实例
防御方式:
- 反射:在构造器中判断实例是否已存在,已存在则抛异常
- 序列化:添加
readResolve()方法返回已有实例 - 克隆:重写
clone()方法返回已有实例,或直接抛异常 - 多 ClassLoader:指定同一个 ClassLoader 加载,或使用枚举(天然解决)
Q5:Spring 中的单例 Bean 和 GoF 单例模式有什么区别?
答案:
| 对比维度 | Spring 单例 Bean | GoF 单例模式 |
|---|---|---|
| 范围 | 单例范围是 Spring 容器级别 | 单例范围是 JVM 级别(ClassLoader 级别) |
| 实现方式 | 由 IoC 容器管理,通过 ConcurrentHashMap 缓存 | 在类内部通过静态变量实现 |
| 灵活性 | 可通过 @Scope 切换为 prototype/request/session | 固定为单例,无法切换 |
| 依赖注入 | 支持依赖注入和 AOP 代理 | 不支持 |
| 生命周期 | 由容器管理,支持 InitializingBean/DisposableBean | 与 JVM 生命周期一致 |
核心区别:Spring 的单例是"容器内单例",即同一个 Bean 名称在同一个容器中只有一个实例;而 GoF 单例是"类级别单例",即整个 JVM 中该类只有一个实例。如果有两个 Spring 容器,就会有两个"单例 Bean"。
总结
单例模式是面试最高频的设计模式,其核心考点包括:
- volatile 在 DCL 中的作用(指令重排序 + 可见性)
- 枚举实现单例的优势(天然防反射、防序列化)
- 静态内部类的实现原理(JVM 类加载机制)
- 破坏单例的方式与防御手段
- Spring 单例 Bean 的本质(容器级单例 vs 类级单例)
实际开发中建议:
- 简单场景:使用枚举
- 需要懒加载 + 灵活性:使用静态内部类
- 需要传递参数构造:使用DCL
- 在 Spring 项目中:直接使用 Spring 的单例 Bean,无需手动实现
