一、什么是单例模式?
单例模式(Singleton Pattern)是一种创建型设计模式 ,核心思想是:
保证一个类在整个程序运行期间,有且仅有一个实例,并提供一个全局访问点来获取这个唯一实例。
核心特点
- 唯一实例:类只能被实例化一次。
- 全局访问:提供一个静态方法,让外部能方便地获取这个唯一实例。
- 私有构造 :构造方法私有化,禁止外部通过
new关键字创建实例。
适用场景
- 配置类(如数据库连接配置、系统配置)
- 日志类(全局统一日志输出)
- 线程池、缓存、连接池等需要共享资源的场景
二、单例模式的实现方式
单例模式有多种实现方式,各有优缺点,下面是常见的几种:
1. 饿汉式(Eager Initialization)
特点:类加载时就创建实例,线程安全,但可能造成资源浪费(如果实例一直没被使用)。
java
public class Singleton {
// 类加载时就初始化
private static final Singleton instance = new Singleton();
// 私有构造
private Singleton() {}
// 全局访问点
public static Singleton getInstance() {
return instance;
}
}
2. 懒汉式(Lazy Initialization)
特点 :第一次调用 getInstance() 时才创建实例,节省资源,但非线程安全。
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
// 非线程安全:多线程下可能创建多个实例
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
3. 双重检查锁(Double-Checked Locking)
特点 :结合了懒汉式的延迟加载和线程安全,是最常用的高效实现方式。
java
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()分为三步(分配内存、初始化对象、引用赋值),volatile防止这三步被重排,避免多线程下获取到未初始化的实例。
- 保证变量的可见性 :一个线程修改了
- 双重检查 :
- 第一次检查:快速判断,避免每次都进入同步块,提升性能。
- 第二次检查:在同步块内再次判断,确保只创建一个实例。
4. 静态内部类(Static Inner Class)
特点:利用类加载机制实现延迟加载和线程安全,代码简洁,推荐使用。
java
public class Singleton {
private Singleton() {}
// 静态内部类:只有在被调用时才会加载,实现延迟加载
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
5. 枚举(Enum)
特点 :最简洁、最安全的实现方式,天然防止反射和序列化破坏单例,推荐使用。
java
public enum Singleton {
INSTANCE; // 唯一实例
// 可以添加其他方法
public void doSomething() {
// 业务逻辑代码
}
}
// 使用方式
Singleton.INSTANCE.doSomething();
三、单例模式的破坏与防护
1. 反射破坏
反射可以通过 setAccessible(true) 绕过私有构造,创建多个实例。
防护:
- 在构造方法中判断实例是否已存在,若存在则抛出异常。
java
private Singleton() {
if (instance != null) {
throw new RuntimeException("单例模式禁止反射创建实例");
}
}
- 使用枚举实现,天然防止反射破坏。
2. 序列化破坏
如果单例类实现了 Serializable 接口,反序列化时会创建新的实例。
防护:
- 重写
readResolve()方法,返回单例实例。
java
private Object readResolve() {
return instance;
}
- 使用枚举实现,天然防止序列化破坏。
四、实现方式对比
| 实现方式 | 线程安全 | 延迟加载 | 性能 | 推荐度 |
|---|---|---|---|---|
| 饿汉式 | ✅ | ❌ | 高 | ⭐⭐⭐ |
| 懒汉式 | ❌ | ✅ | 低 | ❌ |
| 双重检查锁 | ✅ | ✅ | 高 | ⭐⭐⭐⭐ |
| 静态内部类 | ✅ | ✅ | 高 | ⭐⭐⭐⭐⭐ |
| 枚举 | ✅ | ✅ | 高 | ⭐⭐⭐⭐⭐ |
最佳实践
- 追求简洁和安全:使用枚举。
- 追求延迟加载和性能:使用静态内部类 或双重检查锁。
五、总结
- 单例模式的核心是保证类只有一个实例,并提供全局访问点,构造方法必须私有化。
- 饿汉式线程安全但无延迟加载,懒汉式有延迟加载但非线程安全,双重检查锁和静态内部类兼顾了线程安全与延迟加载。
- 枚举是最安全的单例实现方式,能天然抵御反射和序列化破坏,推荐优先使用。