# 单例模式有哪几种实现?如何保证线程安全?
7 min read
Table of Contents
单例模式有哪几种实现?如何保证线程安全?
单例模式实现与线程安全保证
【核心定义】
单例模式是一种创建型设计模式,确保一个类只有一个实例,并提供全局访问点。
【关键要点】
1. 饿汉式(线程安全)
- 结论:类加载时立即创建实例,天生线程安全
- 原理:利用JVM类加载机制保证实例的唯一性
- 实现:
public class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton() {} public static Singleton getInstance() { return INSTANCE; }}2. 懒汉式(需额外同步)
- 结论:延迟初始化,需要同步机制保证线程安全
- 原理:首次调用时创建实例,需解决多线程并发问题
- 线程安全实现:
public class Singleton { 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; }}3. 静态内部类(推荐)
- 结论:利用类加载机制实现懒加载且线程安全
- 原理:静态内部类在首次被引用时才加载,由JVM保证线程安全
- 实现:
public class Singleton { private Singleton() {} private static class Holder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; }}4. 枚举(最佳实践)
- 结论:最简洁、安全的单例实现方式
- 原理:枚举实例由JVM保证唯一性,且能防止反射攻击
- 实现:
public enum Singleton { INSTANCE; public void doSomething() { // 业务方法 }}【深度推导/细节】
双重检查锁定(DCL)的演进
Step 1 - 基础懒汉式(线程不安全)
if (instance == null) { instance = new Singleton(); // 多线程下可能创建多个实例}Step 2 - 方法级同步(性能差)
public synchronized static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance;}Step 3 - 双重检查锁定(需volatile)
- 问题根源:
instance = new Singleton()不是原子操作 - 指令重排序风险:
- 分配内存空间
- 初始化对象
- 将引用指向内存地址(可能被重排序到2之前)
- 解决方案:使用
volatile禁止指令重排序
单例破坏与防护
- 反射攻击防护:在构造器中检查实例是否已存在
- 序列化攻击防护:实现
readResolve()方法返回已有实例 - 克隆攻击防护:重写
clone()方法并抛出异常
【关联/对比】
实现方式对比
| 实现方式 | 线程安全 | 懒加载 | 性能 | 防反射 | 防序列化 |
|---|---|---|---|---|---|
| 饿汉式 | ✅ | ❌ | 优 | ❌ | ❌ |
| 懒汉式(DCL) | ✅ | ✅ | 良 | ❌ | ❌ |
| 静态内部类 | ✅ | ✅ | 优 | ❌ | ❌ |
| 枚举 | ✅ | ❌ | 优 | ✅ | ✅ |
与多例模式对比
- 单例:整个应用生命周期只有一个实例
- 多例:有限数量的实例,按需获取(如连接池)
『面试官追问』
Q1:为什么枚举单例是最佳实践?
A:枚举单例由JVM保证实例唯一性,天然防止以下攻击:
- 反射攻击:枚举的构造器是私有的且不能被反射调用
- 序列化攻击:枚举的序列化机制保证反序列化时返回同一实例
- 线程安全:枚举实例的创建由类加载机制保证
Q2:volatile在DCL中的作用是什么?
A:主要解决指令重排序问题:
- 没有volatile时,
instance = new Singleton()可能被重排序为:- 分配内存 → 2. 设置引用 → 3. 初始化对象
- 这可能导致其他线程拿到未完全初始化的对象
- volatile通过内存屏障禁止这种重排序
Q3:静态内部类如何实现懒加载?
A:基于JVM类加载机制:
- 外部类加载时,内部类不会被加载
- 只有当调用
getInstance()时,才会触发Holder类的加载 - 类加载过程是线程安全的,由JVM保证
Q4:单例模式在Spring中是如何应用的?
A:Spring默认使用单例作用域,但实现方式不同:
- Spring的单例是容器级单例,每个容器一个实例
- 通过三级缓存解决循环依赖问题
- 使用ConcurrentHashMap存储单例bean
Q5:如何实现线程安全的延迟初始化单例?
A:推荐优先级:
- 枚举单例(如果不需懒加载)
- 静态内部类(需要懒加载时)
- 双重检查锁定(需考虑volatile和JDK版本)
- Holder模式:结合了静态内部类和枚举的优点
Q6:单例模式的适用场景?
A:
- 配置管理类(全局配置)
- 连接池、线程池
- 日志记录器
- 设备管理器(如打印机)
- 缓存系统
Q7:单例模式的缺点?
A:
- 违反单一职责原则(控制实例创建和业务逻辑)
- 难以进行单元测试(依赖全局状态)
- 隐藏类之间的依赖关系
- 可能成为”上帝对象”,承担过多职责
最佳实践建议:
- 如果不需要懒加载,优先使用枚举单例
- 如果需要懒加载,优先使用静态内部类
- 避免使用复杂的双重检查锁定,除非有特殊性能要求
- 考虑使用依赖注入框架(如Spring)管理单例,而不是手动实现