单例模式(Singleton)
单例模式是最简单、最常用的设计模式之一。它确保一个类只有一个实例,并提供一个全局访问点。
模式定义
单例模式(Singleton Pattern):确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。
核心要点
- 唯一性:类只能有一个实例
- 全局访问:提供一个全局访问点
- 自行创建:类自身负责保存其唯一实例
问题场景
假设我们需要一个配置管理器,用于读取和管理应用程序的配置信息:
// 问题代码:每次调用都创建新实例
public class ConfigManager {
private Properties config;
public ConfigManager() {
// 加载配置文件(耗时操作)
loadConfig();
}
private void loadConfig() {
// 模拟从文件加载配置
config = new Properties();
// ... 读取配置文件
}
public String getConfig(String key) {
return config.getProperty(key);
}
}
// 使用
ConfigManager manager1 = new ConfigManager(); // 加载配置
ConfigManager manager2 = new ConfigManager(); // 再次加载配置(浪费资源)
// manager1 和 manager2 是不同的实例,配置可能不一致
存在的问题:
- 每次创建实例都要加载配置,浪费资源
- 多个实例可能导致配置不一致
- 需要全局共享同一份配置
解决方案
使用单例模式,确保整个应用只有一个 ConfigManager 实例:
public class ConfigManager {
// 1. 私有静态实例
private static ConfigManager instance;
private Properties config;
// 2. 私有构造方法
private ConfigManager() {
loadConfig();
}
// 3. 公共静态获取方法
public static ConfigManager getInstance() {
if (instance == null) {
instance = new ConfigManager();
}
return instance;
}
private void loadConfig() {
config = new Properties();
// 加载配置...
}
public String getConfig(String key) {
return config.getProperty(key);
}
}
// 使用
ConfigManager manager1 = ConfigManager.getInstance();
ConfigManager manager2 = ConfigManager.getInstance();
// manager1 == manager2 返回 true
模式结构
组成部分:
- 私有静态实例:保存唯一的实例
- 私有构造方法:防止外部通过 new 创建实例
- 公共静态方法:提供全局访问点
实现方式
1. 饿汉式(推荐)
在类加载时就创建实例,线程安全,实现简单。
public class Singleton {
// 类加载时就创建实例
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
// 私有构造方法
}
public static Singleton getInstance() {
return INSTANCE;
}
public void doSomething() {
System.out.println("Singleton is working");
}
}
// 使用
Singleton singleton = Singleton.getInstance();
singleton.doSomething();
优点:
- 实现简单
- 线程安全(类加载机制保证)
- 没有同步开销
缺点:
- 类加载时就创建实例,可能造成资源浪费
- 无法实现延迟初始化
2. 懒汉式(简单实现)
在第一次使用时创建实例,实现延迟初始化。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优点:
- 实现延迟初始化
- 节省资源
缺点:
- 线程不安全:多线程环境下可能创建多个实例
3. 懒汉式(同步方法)
通过同步方法保证线程安全。
public class Singleton {
private static Singleton instance;
private Singleton() {}
// 同步方法
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优点:
- 线程安全
- 延迟初始化
缺点:
- 每次获取实例都要同步,性能开销大
- 同步只需要在第一次创建时
4. 双重检查锁(DCL)
只在第一次创建时同步,兼顾性能和线程安全。
public class Singleton {
// volatile 防止指令重排序
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;
}
}
为什么需要 volatile?
new Singleton() 不是原子操作,分为三步:
- 分配内存空间
- 初始化对象
- 将引用指向内存地址
由于指令重排序,可能变成 1→3→2。如果没有 volatile,其他线程可能获取到未完全初始化的对象。
优点:
- 线程安全
- 延迟初始化
- 性能好(只在第一次同步)
缺点:
- 实现稍复杂
- 需要 JDK 1.5+(volatile 语义修正)
5. 静态内部类(推荐)
利用类加载机制保证线程安全,同时实现延迟初始化。
public class Singleton {
private Singleton() {}
// 静态内部类
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
工作原理:
- 外部类加载时,静态内部类不会加载
- 调用
getInstance()时,JVM 加载 Holder 类 - 类加载机制保证线程安全
优点:
- 线程安全
- 延迟初始化
- 实现简单
- 没有同步开销
缺点:
- 无法传递参数给构造方法
6. 枚举(最佳实践)
使用枚举实现单例,是最简洁、最安全的方式。
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("Singleton is working");
}
}
// 使用
Singleton.INSTANCE.doSomething();
优点:
- 线程安全
- 防止反序列化创建新实例
- 防止反射攻击
- 代码简洁
缺点:
- 无法延迟初始化
- 不能继承其他类
防止破坏单例
1. 防止反射攻击
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
// 防止反射创建实例
if (instance != null) {
throw new RuntimeException("单例模式禁止反射创建实例");
}
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
2. 防止反序列化破坏
public class Singleton implements Serializable {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
// 防止反序列化创建新实例
private Object readResolve() {
return INSTANCE;
}
}
实际应用案例
1. 日志管理器
public class Logger {
private static final Logger INSTANCE = new Logger();
private FileWriter writer;
private Logger() {
try {
writer = new FileWriter("app.log", true);
} catch (IOException e) {
throw new RuntimeException("无法打开日志文件", e);
}
}
public static Logger getInstance() {
return INSTANCE;
}
public void log(String message) {
try {
writer.write(LocalDateTime.now() + " - " + message + "\n");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 使用
Logger.getInstance().log("应用程序启动");
2. 数据库连接池
public class ConnectionPool {
private static volatile ConnectionPool instance;
private List<Connection> connections;
private static final int MAX_SIZE = 10;
private ConnectionPool() {
connections = new ArrayList<>();
// 初始化连接池
for (int i = 0; i < MAX_SIZE; i++) {
connections.add(createConnection());
}
}
public static ConnectionPool getInstance() {
if (instance == null) {
synchronized (ConnectionPool.class) {
if (instance == null) {
instance = new ConnectionPool();
}
}
}
return instance;
}
public synchronized Connection getConnection() {
if (connections.isEmpty()) {
throw new RuntimeException("连接池已空");
}
return connections.remove(0);
}
public synchronized void releaseConnection(Connection conn) {
connections.add(conn);
}
private Connection createConnection() {
// 创建数据库连接
return null;
}
}
3. Spring 框架中的单例
Spring 默认将 Bean 配置为单例:
@Component
@Scope("singleton") // 默认就是 singleton
public class UserService {
// Spring 容器保证只有一个实例
}
优缺点分析
优点
- 内存节约:只有一个实例,减少内存开销
- 全局访问:提供全局访问点,方便访问
- 延迟初始化:某些实现支持延迟加载
- 控制实例数量:严格控制实例数量
缺点
- 违反单一职责:类既负责业务逻辑,又负责创建实例
- 难以扩展:没有接口,难以扩展
- 测试困难:单例的全局状态可能导致测试互相影响
- 隐藏依赖:过度使用可能导致代码耦合度高
使用建议
何时使用单例
- 需要严格控制实例数量的场景
- 需要全局共享的资源(配置、日志、连接池)
- 创建对象开销很大的场景
- 工具类、管理类
何时避免使用单例
- 需要多实例的场景
- 需要继承扩展的场景
- 并发环境下需要隔离状态的场景
- 单元测试需要隔离的场景
最佳实践
- 优先使用枚举实现
- 避免过度使用单例
- 考虑使用依赖注入代替单例
- 单例类尽量无状态
与其他模式的关系
| 模式 | 关系 |
|---|---|
| 工厂方法模式 | 可以使用单例来确保工厂只有一个实例 |
| 抽象工厂模式 | 可以使用单例模式实现具体工厂 |
| 建造者模式 | 可以使用单例模式确保指挥者唯一 |
| 外观模式 | 外观对象通常设计为单例 |
小结
单例模式虽然简单,但需要注意线程安全和实现细节:
| 实现方式 | 线程安全 | 延迟加载 | 推荐场景 |
|---|---|---|---|
| 饿汉式 | 是 | 否 | 实例创建开销小 |
| 懒汉式 | 否 | 是 | 不推荐 |
| 同步方法 | 是 | 是 | 性能要求不高 |
| 双重检查锁 | 是 | 是 | 性能要求高 |
| 静态内部类 | 是 | 是 | 大多数场景 |
| 枚举 | 是 | 否 | 最佳实践 |
练习
- 实现一个线程安全的单例配置管理器
- 使用枚举实现一个单例,包含多个方法
- 分析为什么双重检查锁需要 volatile 关键字
- 实现一个数据库连接池的单例模式