单例模式
单例模式是什么
单例模式(Singleton Pattern)是创建型设计模式的核心之一,其核心目标是:保证一个类在整个应用程序生命周期中仅有一个实例对象,并提供一个全局统一的访问入口来获取这个实例。
要实现单例,必须满足这三个关键约束:
私有化构造方法:禁止外部通过 new 关键字创建类的实例(从根源上避免多实例);
类内部维护唯一实例:通过静态变量存储这个唯一实例;
提供公共静态方法:作为全局访问点,返回这个唯一实例。
常见实现方式
单例的实现分 "饿汉式"(立即加载)和 "懒汉式"(延迟加载)两大类,不同方式的核心差异是实例初始化时机和线程安全性。
懒汉式单例
核心定义:懒汉式单例是单例模式的延迟加载实现范式,其核心特征为按需实例化。
核心特点:
- 实例化时机:遵循延迟初始化原则,不会在类加载阶段创建实例,仅当外部首次调用获取实例的公共方法时,才触发实例的创建;若该实例始终未被调用,则不会完成初始化。
- 线程安全约束:在多线程并发场景下,基础版懒汉式存在线程安全风险 ------ 若多个线程同时调用获取实例的方法,且此时实例尚未初始化,会导致多个线程同时进入实例未创建的判断逻辑,进而重复创建实例,破坏单例模式实例唯一性的核心约束。
- 并发解决方案:为保障并发场景下的单例唯一性,需通过加锁机制(如synchronized关键字)解决并发冲突,常见方案包括对获取实例的方法整体加锁、双重检查锁等;加锁虽能保证线程安全,但会引入一定的性能开销。
- 设计本质:属于 "时间换空间" 的设计方案 ------ 通过延迟实例化节省了类加载阶段的内存资源,但需在首次获取实例时承担实例化的时间成本,且并发场景下的加锁操作会额外增加时间开销。
传统实现代码:
java
public class Singleton {
//设置私有构造方法
private Singleton(){
}
//声明一个Singleton对象为obj
private static Singleton obj;
//加锁保证obj只能实例化一次
public static synchronized Singleton getInstance(){
if(obj == null){
obj = new Singleton();
}
return obj;
}
}
- 性能问题
将 synchronized 关键字直接修饰获取实例的 getInstance() 方法。该实现虽能保证线程安全,但存在显著的性能缺陷,无论单例实例是否已创建,每次调用 getInstance() 方法时,所有线程都需竞争 Singleton.class 对应的类锁,强制串行化执行方法逻辑。即便实例早已初始化完成,线程仍需经历加锁 / 解锁的开销,在高并发场景下会大幅降低方法调用效率。 - 锁粒度优化逻辑
加锁的核心目标是防止实例未初始化时,多线程并发调用 getInstance() 导致重复创建实例,而非对整个方法的执行过程做串行化控制。因此,优化的核心思路是缩小锁粒度:仅对实例化对象的核心代码块加锁,而非修饰整个 getInstance() 方法。
优化实现代码:
java
public class Singleton {
//设置私有构造方法
private Singleton(){
}
private volatile static Singleton obj;
//获取实例对象的方法
public static Singleton getInstance(){
//如果已有实例则直接返回,不走锁
if(obj==null){
//仅在没生成实例时加锁控制,使并发访问串行化
synchronized(Singleton.class){
//多个线程会按序执行到此处,需要再次检查是否已经实例化
if(obj==null){
obj = new Singleton();
}
}
}
return obj;
}
}
- 第一次判空(
if(obj==null)):若实例已完成初始化,直接返回实例,规避锁竞争的开销,确保高并发场景下获取实例的性能; - 类级锁控制(
synchronized(Singleton.class)):仅当实例未初始化时,才对类对象加锁,将并发线程的实例化操作串行化,从根源避免多线程重复创建实例; - 第二次判空(锁内
if(obj==null)):防止多个等待锁的线程在锁释放后重复执行实例化逻辑(例如线程 A 获取锁并创建实例,线程 B 等待锁后若不二次检查,会再次创建实例);
volatile 关键字的核心作用:修饰实例变量obj可防止 JVM 对 obj = new Singleton() 执行指令重排。
该语句实际分为「分配内存→初始化对象→引用赋值」三步,若未加 volatile,可能出现其他线程获取到已赋值但未完成初始化的不完整实例,是 DCL 实现真正线程安全的必要条件。
饿汉式单例
饿汉式单例是单例模式的立即加载实现范式,核心特征为在类加载阶段完成唯一实例的创建,依托 JVM 原生的类加载机制保证线程安全。
核心特点:
- 实例化时机:初始化饿汉式单例的实例化发生在 JVM 加载该类的初始化阶段(执行
<clinit>()类初始化方法)。只要 JVM 首次加载该类,就会立即执行静态实例变量的赋值逻辑,完成单例对象的创建。 - 线程安全:同一个类的
<clinit>()方法在多线程环境下只会被执行一次(由 JVM 的类加载器加锁保证)。因此饿汉式单例无需通过 synchronized 等锁机制,天然避免了多线程并发创建多个实例的问题,线程安全性是原生级的,无并发风险。 - 设计本质:空间换时间的权衡策略
空间成本:在类加载阶段即完成单例实例的初始化并占用堆内存资源,若该实例全程未被业务逻辑调用,会造成内存资源的无效占用(无法通过常规手段释放);
时间收益:后续所有获取实例的操作,均为无锁化的直接返回已初始化实例,既无需执行实例化逻辑的时间成本,也无任何并发同步开销,在高并发场景下能实现实例获取性能的最优解。
传统实现代码:
java
public class Singleton {
//私有构造方法
private Singleton(){
};
//类加载时就实例化对象 加static
private static Singleton obj=new Singleton();
public static Singleton getInstance(){
return obj;
}
}
核心问题:
上述实现中,通过static关键字将实例变量声明为类级静态成员,依据 JVM 类加载机制,该实例会在类初始化阶段完成创建。当外部调用该类的任意静态方法、访问该类的静态成员变量,甚至仅通过Class.forName()加载该类时,都会触发类初始化,进而提前创建单例实例。这会导致核心问题:若业务仅需调用该类的其他静态方法(无需使用单例实例),单例对象仍会被强制实例化并占用内存,造成内存资源的无效占用。
优化思路与原理:
为解决非按需初始化的资源浪费问题,同时保留其 JVM 原生线程安全的优势,可采用静态内部类(Holder 模式) 实现单例,核心原理依托 JVM 的类加载特性:
JVM 加载外部类时,不会主动加载其静态内部类,仅当外部代码首次访问静态内部类的静态成员时,才会触发内部类的加载与初始化;静态内部类的初始化过程由 JVM 保证线程安全(<clinit>() 方法仅执行一次),无需额外加锁;最终实现仅当调用 getInstance() 时才初始化单例实例的延迟加载,同时规避非必要实例化的资源浪费。
优化实现代码:
java
public class Singleton {
// 私有构造函数
private Singleton() {
}
// 静态内部类
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}