# 单例模式有哪几种实现?如何保证线程安全?

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() 不是原子操作
  • 指令重排序风险:
    1. 分配内存空间
    2. 初始化对象
    3. 将引用指向内存地址(可能被重排序到2之前)
  • 解决方案:使用volatile禁止指令重排序

单例破坏与防护

  1. 反射攻击防护:在构造器中检查实例是否已存在
  2. 序列化攻击防护:实现readResolve()方法返回已有实例
  3. 克隆攻击防护:重写clone()方法并抛出异常

【关联/对比】

实现方式对比

实现方式线程安全懒加载性能防反射防序列化
饿汉式✅❌优❌❌
懒汉式(DCL)✅✅良❌❌
静态内部类✅✅优❌❌
枚举✅❌优✅✅

与多例模式对比

  • 单例:整个应用生命周期只有一个实例
  • 多例:有限数量的实例,按需获取(如连接池)

『面试官追问』

Q1:为什么枚举单例是最佳实践?

A:枚举单例由JVM保证实例唯一性,天然防止以下攻击:

  1. 反射攻击:枚举的构造器是私有的且不能被反射调用
  2. 序列化攻击:枚举的序列化机制保证反序列化时返回同一实例
  3. 线程安全:枚举实例的创建由类加载机制保证

Q2:volatile在DCL中的作用是什么?

A:主要解决指令重排序问题:

  • 没有volatile时,instance = new Singleton()可能被重排序为:
    1. 分配内存 → 2. 设置引用 → 3. 初始化对象
  • 这可能导致其他线程拿到未完全初始化的对象
  • volatile通过内存屏障禁止这种重排序

Q3:静态内部类如何实现懒加载?

A:基于JVM类加载机制:

  • 外部类加载时,内部类不会被加载
  • 只有当调用getInstance()时,才会触发Holder类的加载
  • 类加载过程是线程安全的,由JVM保证

Q4:单例模式在Spring中是如何应用的?

A:Spring默认使用单例作用域,但实现方式不同:

  • Spring的单例是容器级单例,每个容器一个实例
  • 通过三级缓存解决循环依赖问题
  • 使用ConcurrentHashMap存储单例bean

Q5:如何实现线程安全的延迟初始化单例?

A:推荐优先级:

  1. 枚举单例(如果不需懒加载)
  2. 静态内部类(需要懒加载时)
  3. 双重检查锁定(需考虑volatile和JDK版本)
  4. Holder模式:结合了静态内部类和枚举的优点

Q6:单例模式的适用场景?

A:

  1. 配置管理类(全局配置)
  2. 连接池、线程池
  3. 日志记录器
  4. 设备管理器(如打印机)
  5. 缓存系统

Q7:单例模式的缺点?

A:

  1. 违反单一职责原则(控制实例创建和业务逻辑)
  2. 难以进行单元测试(依赖全局状态)
  3. 隐藏类之间的依赖关系
  4. 可能成为”上帝对象”,承担过多职责

最佳实践建议:

  • 如果不需要懒加载,优先使用枚举单例
  • 如果需要懒加载,优先使用静态内部类
  • 避免使用复杂的双重检查锁定,除非有特殊性能要求
  • 考虑使用依赖注入框架(如Spring)管理单例,而不是手动实现
My avatar

文章看完了,不打算去别的篇章转转吗?👀 感谢你的耐心阅读!如果你有问题想探讨,或者发现了文中的 Bug,请务必通过页脚的链接联系我


More Posts

# 到底什么是 TCP 连接?

10 min read

TCP 连接是通信双方在传输数据前,通过三次握手建立的、包含双方状态信息(如序列号、窗口大小等)的虚拟通信通道,确保数据传输的可靠性和有序性。

Read

# 常用的 JVM 配置参数有哪些?

6 min read

JVM 配置参数是用于调整 Java 虚拟机运行时行为、性能优化和故障排查的各类选项,主要包括标准参数、非标准参数(-X)和非稳定参数(-XX)三大类。

Read

Comments