一句话总结
单例模式确保一个类只有一个实例,并提供全局访问点。五种写法:饿汉式(类加载时创建,线程安全但可能浪费内存)、懒汉式(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() 对枚举抛异常)和防序列化破坏。