单例模式?

2025年 阅读约 12 分钟 面试指南 · 设计模式

深入解析单例模式五种写法:饿汉式、懒汉式、双重检查锁DCL、静态内部类、枚举单例,反射与序列化破坏及防护,Spring中单例Bean的实现,附面试模拟问答。

一句话总结

单例模式确保一个类只有一个实例,并提供全局访问点。五种写法:饿汉式(类加载时创建,线程安全但可能浪费内存)、懒汉式(synchronized 方法,性能差)、DCL 双重检查锁(volatile + 两次 null 检查,推荐)、静态内部类(利用类加载机制保证线程安全,推荐)、枚举单例(天然防反射和序列化破坏,最安全,Effective Java 推荐)。Spring 中 Bean 默认是单例,通过 ConcurrentHashMap 缓存实现。

初级理解

饿汉式

public class Singleton { // 类加载时就创建实例 private static final Singleton INSTANCE = new Singleton(); private Singleton() {} public static Singleton getInstance() { return INSTANCE; } } # 优点:线程安全(类加载机制保证) # 缺点:类加载时就创建,可能浪费内存(如果一直没用到) # 适用:实例创建开销小、一定会用到的场景

懒汉式(线程安全版)

public class Singleton { private static Singleton instance; private Singleton() {} // synchronized 保证线程安全,但每次调用都加锁 public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } # 优点:延迟加载(用到才创建) # 缺点:synchronized 锁整个方法,并发性能差 # 每次 getInstance() 都要获取锁

中级深入

DCL 双重检查锁(推荐)

public class Singleton { // volatile 防止指令重排序 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; } } # 为什么需要 volatile? # instance = new Singleton() 分三步: # 1. 分配内存空间 # 2. 调用构造方法初始化对象 # 3. 将 instance 指向内存地址 # JVM 可能重排序为 1→3→2 # 线程A执行到步骤3(instance非null但未初始化) # 线程B第一次检查发现 instance != null,直接返回 # 线程B拿到未初始化的对象 → 出错! # volatile 禁止指令重排序,保证 1→2→3 顺序执行 # 为什么两次 null 检查? # 第一次:避免已经创建后还加锁(性能优化) # 第二次:保证只有一个线程创建实例(线程安全)

静态内部类(推荐)

public class Singleton { private Singleton() {} // 静态内部类,类加载时不初始化 private static class Holder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; // 首次访问时才加载 Holder 类 } } # 原理: # 1. Singleton 类加载时,Holder 不会被加载 # 2. 首次调用 getInstance() 时,JVM 加载 Holder 类 # 3. 类加载过程是线程安全的(JVM 保证) # 4. INSTANCE 在 Holder 类加载时初始化 # 优点: # 1. 延迟加载(Holder 类用到才加载) # 2. 线程安全(JVM 类加载机制保证) # 3. 无锁,性能好 # 4. 代码简洁 # 缺点: # 1. 无法传参(构造方法不能带参数) # 2. 反射可以破坏(需在构造方法加防护)

高级进阶

枚举单例(最安全)

public enum Singleton { INSTANCE; public void doSomething() { System.out.println("do something"); } } # 使用: Singleton.INSTANCE.doSomething(); # 为什么枚举单例最安全? # 1. 天然防反射攻击 # Constructor.newInstance() 源码中对枚举类型做了判断 # if (clazz.getModifiers() & Modifier.ENUM) != 0) # throw new IllegalArgumentException("Cannot reflectively # create enum objects"); # # 2. 天然防序列化破坏 # 枚举的序列化/反序列化由 JVM 特殊处理 # 反序列化时不会创建新对象,直接返回枚举常量 # # 3. 线程安全 # 枚举实例在类加载时创建,JVM 保证线程安全 # Effective Java 作者 Josh Bloch 推荐: # "单元素的枚举类型是实现单例的最佳方式"

反射破坏与防护

# 反射破坏单例 Class<Singleton> clazz = Singleton.class; Constructor<Singleton> constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); // 绕过 private Singleton s1 = constructor.newInstance(); // 新实例! Singleton s2 = Singleton.getInstance(); System.out.println(s1 == s2); // false!单例被破坏 # 防护方案(构造方法加判断) public class Singleton { private static volatile Singleton instance; private static boolean created = false; private Singleton() { if (created) { throw new RuntimeException("单例已创建,禁止反射调用"); } created = true; } } # 序列化破坏与防护 # 反序列化时会创建新对象,破坏单例 # 解决方案:添加 readResolve() 方法 public class Singleton implements Serializable { private static final Singleton INSTANCE = new Singleton(); // 反序列化时调用此方法,返回已有实例 private Object readResolve() { return INSTANCE; } } # 原理:ObjectInputStream.readOrdinaryObject() # 如果类有 readResolve() 方法,反序列化后调用它 # 用 readResolve() 返回值替换反序列化创建的对象

Spring 单例 Bean 实现

# Spring 单例 Bean 不是用传统单例模式实现的 # 而是通过容器缓存(ConcurrentHashMap) # DefaultSingletonBeanRegistry 核心代码: public class DefaultSingletonBeanRegistry { // 一级缓存:存储完全初始化的单例 Bean private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); public Object getSingleton(String beanName) { return singletonObjects.get(beanName); } protected void addSingleton(String beanName, Object bean) { singletonObjects.put(beanName, bean); } } # Spring 单例 vs 传统单例: # 1. Spring 单例是"容器内单例",不是 JVM 级别单例 # 2. 同一个类可以有多个不同名称的 Bean 实例 # 3. 通过 ConcurrentHashMap 缓存,不是 static 变量 # 4. 作用域:singleton(默认)、prototype、request、session

实战场景

# 场景1:配置管理器 public enum ConfigManager { INSTANCE; private Properties props = new Properties(); ConfigManager() { // 加载配置文件 try (InputStream is = ConfigManager.class .getResourceAsStream("/config.properties")) { props.load(is); } catch (IOException e) { throw new RuntimeException(e); } } public String get(String key) { return props.getProperty(key); } } # 场景2:数据库连接池 public class ConnectionPool { private static class Holder { private static final ConnectionPool INSTANCE = new ConnectionPool(); } public static ConnectionPool getInstance() { return Holder.INSTANCE; } // 连接池逻辑... } # 场景3:Spring Bean 默认单例 @Service // 默认 @Scope("singleton") public class UserService { // Spring 容器保证只有一个实例 }

面试模拟

面试官:单例模式有哪些写法?DCL 为什么需要 volatile?

你:五种:饿汉式、懒汉式(synchronized)、DCL、静态内部类、枚举。DCL 的 volatile 防止指令重排序:new 操作分三步(分配内存→初始化→指向引用),JVM 可能重排为分配→指向→初始化,导致其他线程拿到未初始化对象。volatile 禁止这种重排序。

面试官:如何防止反射和序列化破坏单例?

你:反射破坏:构造方法加标志位判断,已创建则抛异常。序列化破坏:添加 readResolve() 方法返回已有实例。最彻底的方案是用枚举单例,JVM 底层天然防反射(Constructor.newInstance() 对枚举抛异常)和防序列化破坏。