单例模式作为创建型模式中最简单且最常用的设计模式之一,其核心目标是确保一个类在整个系统中仅存在一个实例,并提供一个全局访问点。
一、 核心定义与设计意图
- 核心定义:单例模式通过私有化构造函数,并在类内部控制实例的创建过程,强制外界只能通过特定方法获取该实例。
- 设计动机:在软件系统中,某些资源(如数据库连接池、线程池、配置管理器、日志对象)往往只需要一个实例。如果创建多个实例,会导致资源浪费、数据一致性冲突或系统行为异常。
二、 单例模式的实现要素
要实现一个标准的单例模式,必须具备以下三个关键点:
- 私有构造函数 :防止外部通过
new关键字创建实例。 - 私有静态变量:用于存储唯一的类实例。
- 公有静态获取方法 :提供全局统一的访问入口(通常命名为
getInstance())。
三、 主流实现方式及其演进
根据其实例化的时机和并发安全性,单例模式主要分为以下几种实现方式:
饿汉式(Eager Initialization)
-
实现机制:在类加载时即完成实例的初始化。
-
优点:线程安全(由 JVM 类加载机制保证),实现简单。
-
缺点:不支持延迟加载。如果该实例占用资源大且未被使用,会导致内存浪费。
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
懒汉式(Lazy Initialization - 线程不安全)
-
实现机制 :在第一次调用
getInstance()时才创建实例。 -
缺点:在多线程并发环境下,可能会创建多个实例,违反单例初衷。
public class Singleton {
private static Singleton instance;
private Singleton (){}public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }}
懒汉式(Lazy Initialization - 线程安全)
- 实现机制: 在
getInstance()方法声明上使用synchronized关键字。由于该方法通常是静态的,这相当于对 类对象(Class Object) 加锁。这确保了在任何时刻,只能有一个线程进入该方法体执行实例化或返回逻辑。 - 缺点:
-
-
高竞争下的低效:该方式的锁粒度过大。实际上,同步锁只有在"第一次创建实例"时才是必要的。
-
读取性能损耗:一旦实例被创建,后续的调用本质上只是"读取"操作(只读不写),理应支持高并发。但在同步方法模式下,即便实例早已存在,所有获取实例的线程仍需排队等待锁的释放,导致了严重的锁竞争(Lock Contention),从而降低了系统的吞吐量和执行效率。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
-
双重检查锁(Double-Checked Locking, DCL)
-
实现机制 :结合了懒加载和同步锁。通过两次判断
if (instance == null),在保证线程安全的同时减少了同步开销。 -
关键点 :必须使用
volatile关键字防止指令重排序。public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
静态内部类(Static Inner Class)
-
实现机制 :利用 JVM 加载内部类的延迟触发机制。只有在调用
getInstance()引用内部类时,才会初始化实例。 -
评价 :推荐方式之一。兼顾了延迟加载和线程安全,代码优雅。
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
枚举单例(Enum Singleton)
-
实现机制:利用 Java 枚举特性。
-
评价 :最安全的方式。天然防止反射攻击和序列化破坏,由 JVM 底层保证绝对的单例。
public enum Singleton {
INSTANCE;
public void businessMethod() { ... }
}
四、 单例模式的安全性评估
在复杂的工业级环境中,单例的"唯一性"可能受到以下因素挑战:
- 反射攻击(Reflection Attack):
-
- 通过
setAccessible(true)可以强行调用私有构造函数。 - 对策:枚举方式天然免疫;普通方式需在构造函数中增加逻辑判断,若实例已存在则抛出异常。
- 通过
- 序列化破坏(Serialization):
-
- 当单例对象被序列化再反序列化时,会产生新的实例。
- 对策 :在类中增加
readResolve()方法返回已有实例,或使用枚举。
- 多类加载器环境:
-
- 若不同的类加载器加载了同一个单例类,可能会产生多个实例。
五、 适用场景规约
- 资源管理类 :如
DataSource(数据源)、ThreadPool(线程池)。 - 状态同步类 :如配置信息读取类(
ConfigManager)、全局计数器。 - 重量级对象:创建开销巨大,且在业务逻辑中需要频繁复用的对象。
六、 总结与设计权衡
- 优先选型 :若无特殊需求,首选 静态内部类 或 枚举 实现。
- 原则权衡:
-
- 单例模式在一定程度上违反了 单一职责原则(它既负责业务逻辑,又负责管理自身的生命周期)。
- 单例模式难以扩展(违反 开闭原则),且在单元测试中难以被 Mock。
- 架构建议 :在现代框架(如 Spring)中,推荐将 Bean 的作用域设置为
Singleton,由容器统一管理实例生命周期,而非手动编写单例逻辑。