单例模式
问题
什么是单例模式?Java 中有几种实现方式?如何保证线程安全?
答案
核心思想
单例模式确保一个类只有一个实例,并提供全局访问点。
实现方式对比
| 实现方式 | 线程安全 | 懒加载 | 防反射 | 防序列化 | 推荐度 |
|---|---|---|---|---|---|
| 饿汉式 | ✅ | ❌ | ❌ | ❌ | ★★★ |
| 懒汉式(synchronized) | ✅ | ✅ | ❌ | ❌ | ★★ |
| 双重检查锁(DCL) | ✅ | ✅ | ❌ | ❌ | ★★★★ |
| 静态内部类 | ✅ | ✅ | ❌ | ❌ | ★★★★ |
| 枚举 | ✅ | ❌ | ✅ | ✅ | ★★★★★ |
饿汉式
类加载时就创建实例,天然线程安全,但不能懒加载。
饿汉式单例
public class Singleton {
// 类加载时立即初始化
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
双重检查锁(DCL)
兼顾线程安全和性能,是最常见的面试答案。
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() 不是原子操作,JVM 可能重排序为:
- 分配内存
- 将引用指向内存(此时 instance != null,但还没初始化)
- 执行构造方法
volatile 禁止 2 和 3 重排序,确保其他线程拿到的是完全初始化的对象。详见 volatile 关键字。
静态内部类
利用类加载机制保证线程安全,同时实现懒加载。
静态内部类单例
public class Singleton {
private Singleton() {}
// 内部类在第一次被使用时才加载
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
枚举(推荐)
《Effective Java》推荐的方式,天然防止反射和序列化破坏。
枚举单例
public enum Singleton {
INSTANCE;
private final Connection connection;
Singleton() {
// 初始化资源
connection = createConnection();
}
public Connection getConnection() {
return connection;
}
}
// 使用
Singleton.INSTANCE.getConnection();
Spring 中的单例
Spring Bean 默认是单例的(@Scope("singleton")),但它是 Spring 容器管理的单例,不是 JVM 层面的单例——同一个类可以在不同的 Spring 容器中有不同实例。
Spring 单例 Bean
@Service // 默认单例
public class UserService {
// Spring IoC 容器保证只创建一个实例
}
常见面试问题
Q1: 为什么 DCL 中需要两次 if 判断?
答案:
- 第一次
if:快速路径,已经创建的情况下直接返回,不用进入 synchronized 加锁 - 第二次
if:线程 A 和 B 同时通过第一次if,A 先拿到锁创建了实例,B 拿到锁后需要再检查一次避免重复创建
Q2: 如何破坏单例?如何防御?
答案:
两种破坏方式:
- 反射:通过
Constructor.newInstance()调用私有构造器 - 序列化:反序列化会创建新对象
防御方式:
// 防反射:在构造器中检查
private Singleton() {
if (INSTANCE != null) {
throw new RuntimeException("不允许反射创建");
}
}
// 防序列化:添加 readResolve 方法
private Object readResolve() {
return INSTANCE;
}
枚举单例天然免疫这两种攻击。
Q3: 单例模式在 Java 中有哪些典型应用?
答案:
Runtime.getRuntime():饿汉式- Spring IoC 容器中的 Bean:容器级单例
- 数据库连接池、线程池
- 日志对象(如
LoggerFactory)
Q4: 单例模式有什么缺点?
答案:
- 难以测试:全局状态不利于单元测试的隔离
- 隐藏依赖:通过
getInstance()获取,依赖关系不透明 - 违反单一职责:既管理实例生命周期,又承担业务逻辑
在 Spring 项目中,优先使用 Spring IoC 管理的单例 Bean,而不是自己实现单例模式。