📝502 字⏱️3 分钟📅2025-12-16📄Software Architecture#Java / Design Patterns / Singleton / Best Practices / Multi-threading
Java 设计模式详解:单例模式 (Singleton Pattern)
单例模式 (Singleton Pattern)
模式定义
单例模式(Singleton Pattern)是一种创建型设计模式,其核心目的是确保一个类在整个系统中只有一个实例,并提供一个全局访问点。
核心三要素
- 唯一性:确保一个类只有一个实例。
- 自控性:类必须自行创建这个实例(Self-instantiation)。
- 全局性:必须向整个系统提供这个实例。
实现原理与规范
如何确保唯一实例?
要实现单例,必须遵循以下代码规范:
- 私有构造方法:使用
private修饰构造函数,禁止外部通过new关键字实例化。 - 私有静态变量:使用
private static成员变量持有当前类的唯一实例。 - 公有静态方法:提供一个
public static方法(通常命名为getInstance()),向外界返回该实例。
典型应用场景
单例模式通常用于管理无状态的工具类或共享资源,因为有状态的单例在多线程环境下容易产生数据竞争。
- 序列号生成器:保证ID全局唯一。
- Web 页面计数器:确保计数准确,不因刷新而重置。
- 资源管理器:如 IO 读写、数据库连接池(Database Connection Pool)。
- 日志应用:Log4j 等日志框架,保证日志文件的独占读写。
- 配置中心:全局配置文件的读取与缓存。
单例模式的优缺点
优点
- 资源高效:对于重量级资源(如数据库连接、线程池),避免了频繁创建和销毁的开销。
- 数据一致性:全局唯一的实例可以避免多重状态导致的逻辑冲突。
- 避免竞争:在多线程环境下,集中管理对共享资源的访问(如写文件)。
- 简化访问:提供了全局访问点,降低了模块间的耦合。
潜在风险(多实例可能性)
虽然单例模式旨在保证唯一性,但在以下极端情况下可能失效:
- 分布式环境:每个 JVM 都有自己的单例实例。
- 类加载器:同一个 JVM 中,不同的 ClassLoader 加载同一个类,会产生不同的实例。
- 序列化与反序列化:反序列化默认会创建新对象(需重写
readResolve方法)。
核心实现方式详解
1. 饿汉式 (Eager Initialization)
类加载时就完成实例化。
class Singleton {
// 类加载时立即初始化,线程安全
private static Singleton singleton = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return singleton;
}
}
- 优点:实现简单,JVM 层面保证线程安全。
- 缺点:若该类从未被使用,会造成内存浪费。
2. 懒汉式 (Lazy Initialization)
第一次调用时才初始化。
class Singleton2 {
private static Singleton2 singleton2;
private Singleton2() {}
// 使用 synchronized 锁住整个方法,防止多线程同时进入导致创建多个实例
public synchronized static Singleton2 getInstance() {
if (singleton2 == null) {
singleton2 = new Singleton2();
}
return singleton2;
}
}
- 优点:延迟加载,节约资源。
- 缺点:锁粒度太大,每次获取实例都要加锁,性能极差,不推荐在高并发场景使用。
3. 双重检查锁 (Double-Checked Locking, DCL)
懒汉式的性能优化版本。
class Singleton3 {
// 必须使用 volatile 关键字,防止指令重排
private volatile static Singleton3 singleton3;
private Singleton3() {}
public static Singleton3 getInstance() {
// 第一重检查:如果已经创建,直接返回,避免不必要的锁
if (singleton3 == null) {
synchronized(Singleton3.class) {
// 第二重检查:防止A、B线程同时通过第一层检查,A释放锁后B重复创建
if (singleton3 == null) {
singleton3 = new Singleton3();
}
}
}
return singleton3;
}
}
💡 深度解析:为什么需要 volatile?
在 singleton3 = new Singleton3(); 这行代码执行时,JVM 实际上进行了三步操作:
- 分配内存 (Allocate memory)。
- 初始化对象 (Initialize object)。
- 指向地址 (Point pointer to memory)。
问题:如果没有 volatile,CPU 或编译器可能进行指令重排序,将顺序变为 1 -> 3 -> 2。
- 场景:线程 A 执行了 1 和 3(此时
singleton3已经非 null,但未初始化),然后时间片结束。 - 后果:线程 B 进来判断
singleton3 != null,直接拿走了一个未初始化的对象去使用,导致空指针异常或其他错误。 - 解决:
volatile关键字通过内存屏障禁止指令重排序,保证初始化完成后才赋值。
4. 静态内部类 (Static Inner Class)
推荐的优雅实现方式。
public class InnerClassSingleton {
private InnerClassSingleton() {
// 防止反射攻击(可选安全防御)
if (SingletonHolder.INSTANCE != null) {
throw new IllegalStateException("Singleton already initialized");
}
}
// 静态内部类:只有在调用 getInstance 时才会被加载
private static class SingletonHolder {
// 由 JVM 类加载机制保证线程安全
static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
- 优点:结合了饿汉式的线程安全(JVM 机制)和懒汉式的延迟加载优势。
5. 枚举 (Enum)
《Effective Java》作者推荐的最佳方式。
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
System.out.println("Processing...");
}
}
- 优点:
- 绝对防止多次实例化:即使是反射也无法破坏枚举的单例性。
- 自动支持序列化:无需担心反序列化生成新对象。
框架中的单例模式
在现代框架中,单例对象的管理通常交给容器(IoC Container)。
Spring Framework
Spring 默认的 Bean 作用域(Scope)就是单例。
@Configuration
public class AppConfig {
@Bean // 默认 scope = singleton
public DataSource dataSource() {
// 由 Spring 容器管理,确保全局只有一个 HikariDataSource 实例
return new HikariDataSource();
}
}
总结
| 实现方式 | 线程安全 | 延迟加载 | 性能 | 推荐指数 |
|---|---|---|---|---|
| 饿汉式 | 是 | 否 | 高 | ⭐⭐⭐ |
| 懒汉式 (Sync方法) | 是 | 是 | 低 | ⭐ |
| 双重检查锁 (DCL) | 是 | 是 | 高 | ⭐⭐⭐⭐ |
| 静态内部类 | 是 | 是 | 高 | ⭐⭐⭐⭐⭐ |
| 枚举 | 是 | 否 | 高 | ⭐⭐⭐⭐⭐ |
最佳实践建议:
- 如果不需要延迟加载,且为了防止反射/序列化破坏,首选 枚举。
- 如果需要延迟加载,首选 静态内部类。
- 如果在维护旧代码,可能会遇到 DCL,务必检查是否加了
volatile。