Java设计模式—-单例模式

单例模式深度解析

单例模式是最常见的设计模式之一,不仅要会写,更要理解其中的并发陷阱。

一、什么是单例模式?

单例模式(Singleton Pattern)确保一个类只有一个实例,并提供一个全局访问点

1.1 适用场景

  • 配置管理:如 Spring 的 ApplicationContext
  • 线程池:避免频繁创建销毁
  • 数据库连接池:复用连接资源
  • 日志对象:全局统一的日志记录器
  • 缓存:全局共享的缓存实例

1.2 核心要素

  1. 私有构造函数:防止外部 new 实例
  2. 私有静态实例:保存唯一实例
  3. 公有静态方法:提供全局访问点

二、六种实现方式

2.1 饿汉式(Eager Initialization)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {
// 类加载时就初始化
private static final Singleton INSTANCE = new Singleton();

private Singleton() {
// 防止反射破坏单例
if (INSTANCE != null) {
throw new RuntimeException("单例已存在,禁止反射创建");
}
}

public static Singleton getInstance() {
return INSTANCE;
}
}
优点缺点
实现简单类加载即初始化,可能浪费资源
线程安全(JVM 保证)无法传递初始化参数
没有加锁,性能好不支持延迟加载

原理分析:JVM 在类加载阶段会执行 <clinit> 方法初始化静态变量,JVM 保证 <clinit> 方法线程安全。

2.2 懒汉式(Lazy Initialization)

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
private static Singleton instance;

private Singleton() {}

// 方法级别加锁,性能较差
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优点缺点
延迟加载,节省资源每次调用都加锁,性能差
线程安全synchronized 开销大

2.3 双重检查锁定(DCL:Double-Checked Locking)⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}
}

为什么需要两次检查?

1
2
3
4
5
6
7
8
9
10
11
12
13
线程A                              线程B
--------------------------------------------
if (instance == null) TRUE
if (instance == null) TRUE
synchronized 获取锁
if (instance == null) TRUE
instance = new Singleton()
释放锁
synchronized 获取锁
if (instance == null) FALSE ← 第二次检查
释放锁
return instance
return instance

为什么需要 volatile?

instance = new Singleton() 不是原子操作,分三步:

  1. 分配内存空间
  2. 初始化对象
  3. 将引用指向内存地址

JVM 可能发生指令重排序,变成 1→3→2。此时另一线程可能拿到未初始化的对象。

volatile 的作用:

  • 禁止指令重排序(通过内存屏障)
  • 保证可见性

2.4 静态内部类(Holder 模式)⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private Singleton() {}

// 静态内部类,只有在被使用时才会加载
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
优点缺点
延迟加载无法传递初始化参数
线程安全(类加载机制保证)-
无锁,性能优秀-

原理分析

  • SingletonHolder 是静态内部类,只有首次调用 getInstance() 时才会加载
  • JVM 保证类初始化的线程安全性
  • 结合了饿汉式的线程安全和懒汉式的延迟加载优点

这是 推荐 的实现方式,兼顾线程安全、性能和延迟加载。

2.5 枚举单例(Effective Java 推荐)⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public enum Singleton {
INSTANCE;

private Resource resource;

Singleton() {
resource = new Resource();
}

public void doSomething() {
// 业务逻辑
}

public Resource getResource() {
return resource;
}
}

// 使用
Singleton.INSTANCE.doSomething();
优点缺点
代码最简洁不能继承其他类
天然防止反射攻击不适合需要继承的场景
天然防止反序列化破坏-
线程安全-

为什么是最佳实践?

  1. 防反射:JVM 层面禁止通过反射创建枚举实例
  2. 防反序列化:枚举序列化由 JVM 保证,反序列化时返回同一实例
  3. 线程安全:枚举类加载由 JVM 保证线程安全
1
2
3
4
// 尝试反射创建枚举会抛出异常
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance = constructor.newInstance(); // IllegalArgumentException

2.6 容器式单例(Spring 使用方式)

1
2
3
4
5
6
7
8
9
10
11
12
public class SingletonManager {
private static final Map<String, Object> singletonMap = new ConcurrentHashMap<>();

public static void registerSingleton(String key, Object instance) {
singletonMap.putIfAbsent(key, instance);
}

@SuppressWarnings("unchecked")
public static <T> T getSingleton(String key) {
return (T) singletonMap.get(key);
}
}

Spring IoC 容器就是通过 ConcurrentHashMap 管理单例 Bean。

三、如何破坏单例?

3.1 反射攻击

1
2
3
4
5
6
7
8
Singleton instance1 = Singleton.getInstance();

// 反射获取构造器
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance2 = constructor.newInstance();

System.out.println(instance1 == instance2); // false,单例被破坏!

防御方式:在构造函数中检查

1
2
3
4
5
private Singleton() {
if (instance != null) {
throw new RuntimeException("单例已存在");
}
}

3.2 序列化/反序列化攻击

1
2
3
4
5
6
7
8
9
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
oos.writeObject(Singleton.getInstance());

// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"));
Singleton instance2 = (Singleton) ois.readObject();

System.out.println(instance1 == instance2); // false,单例被破坏!

防御方式:添加 readResolve() 方法

1
2
3
4
5
6
7
8
9
10
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;

// ... 其他代码

// 反序列化时直接返回现有实例
private Object readResolve() {
return getInstance();
}
}

四、各实现方式对比

实现方式延迟加载线程安全防反射防序列化推荐度
饿汉式⭐⭐
懒汉式(synchronized)
DCL⭐⭐⭐
静态内部类⭐⭐⭐⭐
枚举⭐⭐⭐⭐⭐

五、面试高频问题

Q1:DCL 为什么要用 volatile?

防止指令重排序,确保其他线程看到的是完全初始化的对象。

Q2:静态内部类为什么是线程安全的?

JVM 在类加载阶段会加锁,保证 <clinit> 方法只执行一次。

Q3:为什么推荐枚举单例?

代码简洁,天然防止反射和序列化攻击,由 JVM 保证线程安全。

Q4:Spring 中的单例和设计模式中的单例有什么区别?

设计模式的单例是 JVM 级别的全局唯一;Spring 的单例是 IoC 容器级别的唯一,同一个类可以在不同容器中有多个实例。