Java设计模式—-单例模式
发表于更新于
字数总计:1.5k阅读时长:5分钟 上海
单例模式深度解析
单例模式是最常见的设计模式之一,不仅要会写,更要理解其中的并发陷阱。
一、什么是单例模式?
单例模式(Singleton Pattern)确保一个类只有一个实例,并提供一个全局访问点。
1.1 适用场景
- 配置管理:如 Spring 的 ApplicationContext
- 线程池:避免频繁创建销毁
- 数据库连接池:复用连接资源
- 日志对象:全局统一的日志记录器
- 缓存:全局共享的缓存实例
1.2 核心要素
- 私有构造函数:防止外部 new 实例
- 私有静态实例:保存唯一实例
- 公有静态方法:提供全局访问点
二、六种实现方式
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 { 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() 不是原子操作,分三步:
- 分配内存空间
- 初始化对象
- 将引用指向内存地址
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();
|
| 优点 | 缺点 |
|---|
| 代码最简洁 | 不能继承其他类 |
| 天然防止反射攻击 | 不适合需要继承的场景 |
| 天然防止反序列化破坏 | - |
| 线程安全 | - |
为什么是最佳实践?
- 防反射:JVM 层面禁止通过反射创建枚举实例
- 防反序列化:枚举序列化由 JVM 保证,反序列化时返回同一实例
- 线程安全:枚举类加载由 JVM 保证线程安全
1 2 3 4
| Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true); Singleton instance = constructor.newInstance();
|
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);
|
防御方式:在构造函数中检查
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);
|
防御方式:添加 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 容器级别的唯一,同一个类可以在不同容器中有多个实例。