单例模式

1、设计初衷
单例模式的核心设计初衷是确保一个类在整个应用程序中只有一个实例 ,并提供一个全局访问点
该设计模式主要解决以下问题:
- 资源共享 :有些对象需要被多个客户端共享使用,如配置管理器、连接池等
- 资源控制 :某些资源有限或昂贵,不应该被频繁创建和销毁
- 状态一致性:确保系统中某些关键组件的状态在任何时刻都是一致的
- 全局访问 :提供一个统一的访问点,简化对象获取和使用的复杂性
2、核心要素
- 私有构造方法 :防止外部通过
new
键字创建实例 - 私有静态成员变量 :存储类的唯一实例
- 公共静态获取方法 :提供全局访问点,返回类的唯一实例
- 实例化控制 :确保无论何时何地调用获取方法,都返回同一个实例
3、主要角色
单例模式是一种简单的设计模式,因此只涉及一个角色:
- Singleton(单例类)
- 负责创建和管理自己的唯一实例
- 提供一个全局访问点
- 控制实例的创建过程(如延迟加载、线程安全等)
4、设计方式
4.1、饿汉式
java
public class Singleton {
// 唯一实例,类加载时初始化
private static final Singleton instance = new Singleton();
// 私有构造方法,防止外部实例化
private Singleton() {}
// 提供全局访问点,返回唯一实例
public static Singleton getInstance() {
// 返回唯一实例
return instance;
}
}
- 特点 :类加载时就完成实例化
- 优点 :线程安全,实现简单
- 缺点 :可能导致内存浪费(即使不使用也会创建实例)
4.2、懒汉式(非线程安全)
java
public class Singleton {
// 唯一实例,延迟初始化
private static Singleton instance;
// 私有构造方法,防止外部实例化
private Singleton() {}
// 提供全局访问点,返回唯一实例
public static Singleton getInstance() {
// 如果实例为空,创建新实例
if (instance == null) {
instance = new Singleton();
}
// 返回唯一实例
return instance;
}
}
- 特点:延迟加载,首次使用时才创建实例
- 优点:节省内存
- 缺点:多线程环境下可能创建多个实例
- 问题根源分析 :多线程场景下可能有多个线程通过
instance == null
的判断条件,导致创建多个实例
比如像这样:
java
static void checkMultiThread() {
for(int i = 0; i < 100; i ++) {
new Thread(() -> {
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
Singleton instance = Singleton.getInstance();
System.out.println(Thread.currentThread().getName() + ": " + instance);
}).start();
}
}

4.3 懒汉式(线程安全,同步方法)
java
public class Singleton {
// 唯一实例,延迟初始化
private static Singleton instance;
// 私有构造方法,防止外部实例化
private Singleton() {}
// 提供全局访问点,返回唯一实例
// 同步方法,确保线程安全
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 特点 :使用
synchronized
关键字保证线程安全 - 优点 :线程安全,延迟加载
- 缺点 :性能较差,每次获取实例都需要同步
- 性能问题的根源 :
synchronized
在方法级别使用,相当于对整个类对象加锁- 任何线程 调用
getInstance()
方法都需要先获取锁 - 多个线程会被强制串行执行,即使只是想要读取已经创建好的实例
- 任何线程 调用
其实只是在第一次创建实例时才需要同步,因此造成了大量不必要的同步开销
在高并发的场景下,会竞争锁 ,未获取到锁的会被阻塞等待,而相应的线程的阻塞和唤醒会带来上下文切换的开销
4.4 双重检查锁定(Double-Checked Locking)
java
public class Singleton {
// 唯一实例,延迟初始化 volatile 确保可见性
private volatile static Singleton instance;
// 私有构造方法,防止外部实例化
private Singleton() {}
// 提供全局访问点,返回唯一实例
public static Singleton getInstance() {
// 第一次检查,避免不必要的同步
if (instance == null) {
synchronized (Singleton.class) {
// 第二次检查,确保线程安全
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
- 特点 :仅在第一次创建实例时进行同步
- 优点 :线程安全,延迟加载,性能较好
- 注意 :
volatile
关键字必不可少,防止指令重排序
4.5 静态内部类方式
java
public class Singleton {
// 私有构造方法,防止外部实例化
private Singleton() {}
// 静态内部类,延迟初始化唯一实例
private static class SingletonHolder {
// 唯一实例,延迟初始化
private static final Singleton INSTANCE = new Singleton();
}
// 提供全局访问点,返回唯一实例
public static Singleton getInstance() {
// 返回唯一实例
return SingletonHolder.INSTANCE;
}
}
- 特点 :利用Java类加载机制实现延迟加载和线程安全
- 优点 :线程安全,延迟加载,性能好
- 优化原理 :
- 减少同步范围 :只在真正需要创建实例时才进行同步
- 双重检查机制 :避免多线程同时创建实例的问题
volatile
关键字的作用
- 可见性 :确保一个线程修改了instance变量后,其他线程能立即看到最新值
- 防止指令重排序 :避免
instance = new Singleton()
这行代码在多线程环境下的执行顺序问题
new Singleton()
实际上包含三个步骤:分配内存空间、初始化对象、将引用指向内存空间不加
volatile
可能导致指令重排序,使得其他线程看到一个未完全初始化的对象
双重检查的工作流程
①第一次检查 :所有线程都会先检查instance == null
- 如果实例已存在(
instance != null
),直接返回实例,无需同步,这是性能优化的关键 - 如果实例不存在,才进入同步代码块
②同步代码块 :多个线程竞争锁,只有一个线程能进入
- 进入同步块的线程进行 第二次检查 (再次确认
instance == null
) - 如果仍为null,才创建实例
③后续调用 :实例创建完成后,所有后续调用只需通过第一次检查,直接返回实例,无需同步
4.6 枚举方式
java
enum Singleton {
INSTANCE;
// 可以添加成员方法
public void doSomething() {
// 实现功能
}
}
- 特点 :利用枚举类的特性实现单例
- 优点 :线程安全,防止反射和序列化破坏单例,实现简单
- 缺点 :不能懒加载
很大程度上是 JVM 本身对其的保证
5. 注意事项
- 线程安全问题 :在多线程环境下必须确保单例的线程安全性
- 防止反射攻击 :可以在构造方法中添加检查,防止通过反射创建多个实例
- 防止序列化攻击 :实现
readResolve()
方法,确保反序列化时返回同一个实例 - 慎重使用 :不要过度使用单例模式,只在确实需要全局唯一实例时使用
- 考虑生命周期 :明确单例实例的生命周期,避免不必要的资源占用
单例模式是Java中使用频率最高的设计模式之一,理解其设计思想和实现方式对于编写高质量的Java程序至关重要
在实际开发中,应根据具体需求选择合适的实现方式,平衡线程安全性、性能和资源占用等因素