Spring单例模式 Spring 中的单例 饿汉式加载 懒汉式加载
目录
核心特性
实现方式详解
1. 饿汉式(Eager Initialization)
2. 懒汉式(Lazy Initialization)
3. 静态内部类(Bill Pugh 实现)
4. 枚举(Enum)
破坏单例的场景及防御
Spring 中的单例
适用场景
缺点
优点
1. 手动注册 Bean(代码配置)
2. 使用 Spring Boot 自动配置
3. 使用 ThreadPoolTaskScheduler(Spring 内置)
4. 检查包扫描路径
常见误区示例
单例模式(Singleton Pattern)是一种创建型设计模式,其核心目标是确保一个类只有一个实例存在,并提供全局唯一的访问点。它常用于资源控制(如数据库连接池、线程池)、配置管理、日志对象等场景。
核心特性
- 唯一性:无论调用多少次,始终返回同一个对象实例。
- 全局访问:通过静态方法或静态变量提供全局访问入口。
- 私有构造:禁止通过
new
关键字直接实例化。
实现方式详解
以下是单例模式的几种典型实现(以 Java 为例):
1. 饿汉式(Eager Initialization)
特点:类加载时立即初始化实例,线程安全但可能浪费资源。
public class EagerSingleton {
// 类加载时直接初始化实例
private static final EagerSingleton instance = new EagerSingleton();
// 私有构造方法
private EagerSingleton() {}
// 全局访问点
public static EagerSingleton getInstance() {
return instance;
}
}
2. 懒汉式(Lazy Initialization)
特点:第一次调用时创建实例,需解决线程安全问题。
- 基础版(非线程安全):
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
- 线程安全版(双重检查锁,Double-Checked Locking):
public class ThreadSafeSingleton {
// volatile 防止指令重排序
private static volatile ThreadSafeSingleton instance;
private ThreadSafeSingleton() {}
public static ThreadSafeSingleton getInstance() {
if (instance == null) {
synchronized (ThreadSafeSingleton.class) {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
}
3. 静态内部类(Bill Pugh 实现)
特点:利用类加载机制保证线程安全,延迟加载且无锁。
public class InnerClassSingleton {
private InnerClassSingleton() {}
// 静态内部类在调用 getInstance() 时才会加载
private static class SingletonHolder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
4. 枚举(Enum)
特点:天然线程安全,防止反射攻击,简洁高效(推荐方式)。
public enum EnumSingleton {
INSTANCE; // 单例实例
public void doSomething() {
// 业务逻辑
}
}
// 使用:EnumSingleton.INSTANCE.doSomething();
破坏单例的场景及防御
- 反射攻击:通过反射调用私有构造方法创建新实例。
-
- 防御:在构造方法中检查实例是否已存在,若存在则抛出异常。
- 反序列化:反序列化可能生成新对象。
-
- 防御:实现
readResolve()
方法返回已有实例。
- 防御:实现
- 克隆:重写
clone()
方法并抛出异常。
Spring 中的单例
在 Spring 框架中,默认的 Bean 作用域是单例(@Scope("singleton")
),但需注意:
- Spring 单例是容器级别的:每个容器中保证唯一,而非 JVM 级别。
- 非严格单例:如果通过
new
或反射创建,仍可能生成多个实例。
适用场景
- 频繁使用的对象(减少资源开销)。
- 需严格控全局状态的场景(如计数器、配置管理)。
- 共享资源访问(如文件系统、数据库连接池)。
缺点
- 违反单一职责原则(同时管理实例化和业务逻辑)。
- 可能隐藏代码耦合。
- 多线程需额外注意同步问题。
单例模式需要谨慎使用,避免滥用导致代码难以测试和维护。以下是不同解决方法的好处及具体场景示例:
优点
1. 手动注册 Bean(代码配置)
好处:完全控制线程池参数(核心线程数、队列容量等),适合需要定制化配置的场景。
@Configuration
public class SchedulerConfig {
@Bean("scheduledExecutorService")
public ScheduledExecutorService scheduledExecutorService() {
return Executors.newScheduledThreadPool(5);
}
}
示例场景:
你需要一个固定大小为5的线程池,并明确指定线程名称格式:
@Bean("scheduledExecutorService")
public ScheduledExecutorService customExecutor() {
return new ScheduledThreadPoolExecutor(5,
new ThreadFactoryBuilder().setNameFormat("scheduler-%d").build());
}
2. 使用 Spring Boot 自动配置
好处:无需手动编码,通过配置文件快速启用调度功能,简化配置。
# application.properties
spring.task.scheduling.pool.size=5
示例场景:
简单应用需要快速启用定时任务,只需在 @Scheduled
注解方法上使用:
@Scheduled(fixedRate = 5000)
public void task() {
System.out.println("Task executed by auto-configured scheduler");
}
3. 使用 ThreadPoolTaskScheduler
(Spring 内置)
好处:与 Spring 生态集成更好,支持更丰富的配置(错误处理、线程管理等)。
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5);
scheduler.setThreadNamePrefix("spring-scheduler-");
return scheduler;
}
示例场景:
需要为不同的任务分配独立的调度器,或在任务失败时记录日志:
@Bean
public ThreadPoolTaskScheduler paymentScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(3);
scheduler.setErrorHandler(t -> logger.error("Payment task failed", t));
return scheduler;
}
4. 检查包扫描路径
好处:避免因配置类未被扫描导致的 Bean 未注册问题。
示例:
假设主启动类在 com.example.app
包下,而配置类在 com.example.config
包中:
@SpringBootApplication
@ComponentScan("com.example.config") // 显式扫描配置包
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
常见误区示例
问题:Bean 名称拼写错误导致依赖注入失败。
// ❌ 错误:Bean 名称写成了 "scheduledExecutor"
@Bean("scheduledExecutor")
public ScheduledExecutorService executor() { ... }
// ❌ 使用时名称不一致
@Autowired
@Qualifier("scheduledExecutorService") // 找不到 Bean!
private ScheduledExecutorService executor;