在软件开发中,有些对象我们只希望它存在一个实例,比如:
- 数据库连接池
- 日志管理器
- 配置中心
- 线程池
如果每次都 new
一个,不仅浪费资源,还可能导致状态混乱。这时,单例模式(Singleton Pattern) 就派上用场了。
什么是单例模式?
定义:
确保一个类只有一个实例,并提供一个全局访问点。
核心目标:
- 私有化构造函数 → 防止外部
new
- 内部创建唯一实例 → 控制实例化过程
- 提供静态方法获取实例 → 全局访问
单例模式的6种实现方式
饿汉式
public class Singleton {
// 类加载时就创建实例
private static final Singleton INSTANCE = new Singleton();
// 私有构造函数
private Singleton() {}
// 提供全局访问方法
public static Singleton getInstance() {
return INSTANCE;
}
}
优点:实现简单,线程安全(类加载机制保证)
缺点:不管用不用,类加载时就创建,可能造成资源浪费
适用场景:对象创建成本低,且一定会用到。
懒汉式
“懒汉式”因其懒加载(Lazy Loading)特性——用的时候才创建实例——被广泛讨论。但“懒汉式”有一个致命问题:默认是非线程安全的!今天,我们就从最基础的懒汉式开始,一步步发现问题、分析问题、解决问题,最终写出一个高效、安全、可靠的单例实现。
最基础的懒汉式
public class Singleton {
private static Singleton instance;
// 私有构造函数
private Singleton() {}
// 提供全局访问点
public static Singleton getInstance() {
if (instance == null) { // 1. 判断是否已创建
instance = new Singleton(); // 2. 创建实例
}
return instance; // 3. 返回实例
}
}
没有则创建,有 则直接返回。但是在多线程环境下会创建多个实例
两个线程同时调用:
时间 | 线程 A | 线程 B |
---|---|---|
t1 | 执行 if (instance == null) → 成立 |
|
t2 | 执行 if (instance == null) → 成立(A还没创建) |
|
t3 | 执行 new Singleton() |
执行 new Singleton() |
t4 | 返回 A 实例 | 返回 B 实例 |
结果:两个线程拿到了不同的实例,违反了单例原则
直接使用Synchronized关键字
synchronized
保证同一时刻只有一个线程能进入该方法。线程安全问题解决。
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
为了保证线程安全,最直接的方法就是加锁。但是无论实例是否已经创建,在获取实例时都需要先获取锁,性能很差。那么改进思路就是 只有在没有创建实例时,才获取锁 去创建实例。
双重检查锁定
我们其实只需要在第一次创建实例时加锁,后续直接返回即可。
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;
}
}
优点:
- 懒加载:用时才创建
- 高性能:只有第一次创建时加锁
- 线程安全:双重检查 + 锁机制保证
为什么需要两次 if (instance == null)
?
- 第一次:避免不必要的加锁(如果实例已存在,直接返回)
- 第二次:防止多个线程同时通过第一层检查后,重复创建实例
为什么必须加 volatile
?
这是最容易被忽略的关键点!
如果不加 volatile
,可能会发生指令重排序(Instruction Reordering),导致其他线程拿到一个未初始化完成的对象。
问题出现在这一行
instance = new Singleton();
这行代码实际上分为三步:
- 分配内存空间
- 调用构造函数初始化对象
- 将
instance
指向内存地址
JVM 可能会重排序为:1 → 3 → 2
如果线程 A 执行到第 3 步(instance
已指向内存),但构造函数还没执行完,此时线程 B 调用 getInstance()
,发现 instance != null
,直接返回——拿到一个半初始化的对象!
防止反射攻击
我们已经通过 双重检查锁定 + volatile 实现了一个线程安全、懒加载的单例模式,看起来“完美”了。但在某些极端场景下,单例仍然可能被破坏——比如,有人通过 反射(Reflection) 强行调用私有构造函数!虽然这在正常业务中很少见,但如果你的系统面临安全风险(如代码注入、反序列化漏洞等),就必须考虑这种可能性。
让我们来模拟一下攻击过程:
// 恶意代码:通过反射破坏单例
public class ReflectionAttack {
public static void main(String[] args) throws Exception {
// 正常获取实例
Singleton s1 = Singleton.getInstance();
// 通过反射获取构造函数
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true); // 暴力破解私有权限
// 强行创建新实例
Singleton s2 = constructor.newInstance();
// 输出结果
System.out.println("s1 == s2? " + (s1 == s2));
// 输出:false 两个不同的实例!
}
}
看!尽管构造函数是 private
的,但反射依然可以绕过访问控制,创建出第二个实例,彻底破坏了单例的唯一性。
如何防御?—— 在构造函数中加判断
private Singleton() {
// 防御反射攻击
if (instance != null) {
throw new RuntimeException("单例模式已被破坏,禁止重复创建!");
}
// 正常初始化...
}
这样,当反射试图第二次创建实例时,构造函数会检测到 instance
已存在,直接抛出异常,阻止攻击。
最终的线程安全+懒加载+放破坏
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
// 防止反射破坏
if (instance != null) {
throw new RuntimeException("单例模式被破坏!");
}
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
// 防止序列化破坏
private Object readResolve() {
return instance;
}
}
如果你希望一劳永逸地防止反射攻击,推荐使用 枚举实现单例:
枚举实现单例模式https://blog.csdn.net/2402_87283759/article/details/151622371?spm=1001.2014.3001.5502