单例模式的介绍和应用场景
单例模式的核心是保证一个类在全局只有一个实例,并提供唯一的访问点。这种模式适用于 “某个对象的状态或资源必须全局唯一,或重复创建会导致资源浪费、逻辑冲突” 的场景。以下是单例模式的典型应用场景:
1 工具类 / 辅助类(无状态或全局配置): 工具类通常封装通用功能(如字符串处理、日期格式化、加密解密等),无需维护独立状态,且重复创建实例会浪费内存。如:日志工具类(如 Logger):全局需要一个统一的日志实例来处理输出(避免多个实例导致日志错乱、重复写入)。加密工具类(如 EncryptUtils):封装加密算法(如 MD5、AES),无需多实例,单例可减少对象创建开销。
2资源管理类(统一管理稀缺资源):源池(如连接池、线程池)用于管理稀缺资源(数据库连接、线程等),需全局唯一实例确保资源分配 / 释放的一致性,避免重复创建池导致资源耗尽。如:数据库连接池(如自定义 DBConnectionPool):全局唯一的连接池负责创建、分配、回收数据库连接,避免多个池同时存在导致连接数超限(数据库通常有最大连接数限制)。 线程池(如 ExecutorService 单例):全局共享一个线程池,统一管理线程生命周期(避免多线程池导致线程资源浪费、调度混乱)。
3状态 / 配置类,全局状态管理需要在应用全局维护一份统一状态(如用户登录信息、全局计数器),单例可作为 “全局注册表” 集中管理这些状态。如:全局计数器(如 GlobalCounter):统计网站访问量、接口调用次数等,单例确保计数逻辑唯一(避免多实例各自计数导致数据错误)。 用户会话管理器(如 SessionManager):在非分布式场景下,管理当前登录用户的会话信息(如 Token 存储),单例确保所有模块访问的是同一批会话数据。
4外部对接类,在非单例模式之下多客户端导致连接冗余、配置不一致(如 AccessKey 错乱),而在单例模式下,避免了上述问题,而且能复用长连接 / 配置,减少初始化开销,确保对接一致性。如:- MQ 客户端(RocketMQ/Kafka Producer)存储客户端(Redis/ES/OSS)第三方 API 客户端(支付 / 短信)
5逻辑调度类,这一类在非单例的模式下会出现任务重复执行(如报表生成多次)、事件漏发 / 乱序等问题,如:定时任务调度器(ScheduledExecutorService/Quartz)事件总线(Guava EventBus)全局任务管理器
单例模式代码实现
单例模式有四种创建模式:懒汉模式,饿汉模式,静态类内部创建,java特有的枚举类创建
懒汉模式
懒汉模式节约内存空间,只有在需要调用的时候才会创建,每次调用都会判断单例类有没有创建,所以使用的效率较低;同时由于在使用的时候创建,导致存在线程安全问题,在一个线程创建实例的过程,另一个线程同时判断实例为null,开始创建实例,就导致了有多个实例,可以使用加锁双重校验的方式保证线程安全;
原始创建代码:
class LazySingleton {
// 私有成员属性
private LazySingleton lazySingleton;
// 私有构造方法
private LazySingleton() {
}
// 公共的获取实例方法
public LazySingleton getLazySingleton() {
// 如果成员属性为空,则创建实例
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
加锁双重校验优化代码
class LazySingleton {
// 私有成员属性,使用volatile可以保证代码的有序性,防止指令重排
private volatile static LazySingleton lazySingleton;
// 私有构造方法
private LazySingleton() {
}
// 公共的获取实例方法
// 使用synchronized + 双重确认机制可以保证线程安全,但有可能存在指令重排,所以lazySingleton用volatile修饰,用内存屏障阻止指令重排
public static LazySingleton getLazySingleton() {
if (lazySingleton == null) {
synchronized (LazySingleton.class) {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
}
}
return lazySingleton;
}
}
饿汉模式
饿汉模式是在类加载的过程中就创建出来,调用的时候不用判断是否创建,使用效率较高。
// 利用类加载机制保证线程安全
class HungrySingleton {
private static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getHungrySingleton() {
return hungrySingleton;
}
}
静态类内部类单例
静态类单例不会有线程安全问题,线程安全由类加载机制担保
/**
* 由静态内部类持有单例对象,并调用外部类的私有构造器初始化,由外部类调用静态内部类的属性
* 本质是一个懒汉模式,在类加载时才会初始化对象
*/
class InnerSingleton implements Serializable {
private static class InnerSingletonHolder {
private static InnerSingleton innerSingleton = new InnerSingleton();
}
private InnerSingleton() {
}
public static InnerSingleton getInnerSingleton() {
return InnerSingletonHolder.innerSingleton;
}
}
枚举单例模式
package com.hy.test.singletonDemo;
public enum EnumSingletonDemo {
INSTANCE;
}
class EnumTest {
public static void main(String[] args) {
EnumSingletonDemo instance = EnumSingletonDemo.INSTANCE;
EnumSingletonDemo instance2 = EnumSingletonDemo.INSTANCE;
System.out.println(instance == instance2);
}
}
单例模式存在的反射攻击问题
反射可以绕过私有构造器的访问限制,通过Constructor.setAccessible(true)强制调用私有构造器创建实例,从而破坏单例的唯一性。例如:
// 反射攻击示例
Class<Singleton> clazz = Singleton.class;
Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true); // 绕过私有访问限制
Singleton instance1 = constructor.newInstance(); // 强制创建实例
Singleton instance2 = Singleton.getInstance(); // 正常获取实例
// 此时 instance1 != instance2,单例被破坏
由此可见,反射生成了一个新的对象,不符合单例模式的定义 解决方法:在私有构造器中添加判断,如果已存在实例对象,抛出异常(也可进行其他操作,根据需求决定)
private Singleton() {
if (instance != null) {
throw new IllegalStateException("单例实例已存在,禁止重复创建");
}
}
但是这个解决办法只能够在饿汉模式和静态内部类模式有效,在懒汉模式中无效,其根本原因在于创建单例的时机不同。
饿汉模式:
实例初始化时机:类加载的 “初始化阶段”,就创建instance,此时构造器被调用,instance被赋值。
反射攻击时:无论何时通过反射调用构造器,instance早已被初始化(非 null),构造器中的检查会触发异常,阻止反射创建新实例。
静态内部类模式:
实例初始化时机:首次调用getInstance()时,静态内部类InnerClass被加载,此时初始化instance(构造器被调用)。
反射攻击时: 若先调用getInstance(),instance已初始化,反射调用构造器会触发检查异常。 若直接反射调用构造器(未调用过getInstance()),此时InnerClass未加载,InnerClass.instance为 null,构造器检查似乎失效? 实际不会:因为反射调用构造器时,会触发StaticInnerClassSingleton的构造器执行,而构造器中检查的是InnerClass.instance。但InnerClass此时未加载,InnerClass.instance尚未初始化(为 null),这时候反射是否能创建实例? 关键:StaticInnerClassSingleton的构造器被调用时,InnerClass的instance还未赋值(因为内部类未加载),但反射创建的实例并不会被赋值给InnerClass.instance。当后续调用getInstance()时,InnerClass加载并初始化instance(再次调用构造器),此时构造器会检查到 “反射创建的实例是否存在” 吗? 这里的防御核心是:InnerClass.instance是单例的唯一源头。无论反射如何创建实例,InnerClass.instance的初始化只会执行一次(类加载的初始化阶段是线程安全且唯一的)。当反射调用构造器时,若InnerClass.instance已存在(即getInstance()被调用过),则检查生效;若InnerClass.instance不存在(未调用getInstance()),反射创建的实例是 “游离的”,但getInstance()后续返回的仍是InnerClass.instance,保证了 “官方入口” 的唯一性。因此,静态内部类的构造器检查仍能有效防御 “通过官方入口获取的实例” 被反射破坏。(一句话:通过反射创建不能再通过getInstance创建,getInstance创建之后,反射不能创建)
懒汉模式:
实例初始化时机:instance在首次调用getInstance()时才被赋值,在这之前instance为null。
反射攻击过程:直接通过反射调用构造器(未调用过getInstance()),此时instance为null,构造器中的检查条件(instance != null)不成立,反射成功创建一个实例(记为reflectInstance)。 后续调用getInstance()时,由于instance仍为null(反射创建的reflectInstance未被赋值给instance),getInstance()会再次创建一个实例(记为normalInstance)。 最终reflectInstance != normalInstance,单例被破坏,且构造器的检查完全失效(因为两次创建时instance都是null)。