跳到主要内容

单例模式(Singleton)

单例模式是最简单、最常用的设计模式之一。它确保一个类只有一个实例,并提供一个全局访问点。

模式定义

单例模式(Singleton Pattern):确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。

核心要点

  1. 唯一性:类只能有一个实例
  2. 全局访问:提供一个全局访问点
  3. 自行创建:类自身负责保存其唯一实例

问题场景

假设我们需要一个配置管理器,用于读取和管理应用程序的配置信息:

// 问题代码:每次调用都创建新实例
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. 分配内存空间
  2. 初始化对象
  3. 将引用指向内存地址

由于指令重排序,可能变成 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 容器保证只有一个实例
}

优缺点分析

优点

  1. 内存节约:只有一个实例,减少内存开销
  2. 全局访问:提供全局访问点,方便访问
  3. 延迟初始化:某些实现支持延迟加载
  4. 控制实例数量:严格控制实例数量

缺点

  1. 违反单一职责:类既负责业务逻辑,又负责创建实例
  2. 难以扩展:没有接口,难以扩展
  3. 测试困难:单例的全局状态可能导致测试互相影响
  4. 隐藏依赖:过度使用可能导致代码耦合度高

使用建议

何时使用单例

  • 需要严格控制实例数量的场景
  • 需要全局共享的资源(配置、日志、连接池)
  • 创建对象开销很大的场景
  • 工具类、管理类

何时避免使用单例

  • 需要多实例的场景
  • 需要继承扩展的场景
  • 并发环境下需要隔离状态的场景
  • 单元测试需要隔离的场景

最佳实践

  1. 优先使用枚举实现
  2. 避免过度使用单例
  3. 考虑使用依赖注入代替单例
  4. 单例类尽量无状态

与其他模式的关系

模式关系
工厂方法模式可以使用单例来确保工厂只有一个实例
抽象工厂模式可以使用单例模式实现具体工厂
建造者模式可以使用单例模式确保指挥者唯一
外观模式外观对象通常设计为单例

小结

单例模式虽然简单,但需要注意线程安全和实现细节:

实现方式线程安全延迟加载推荐场景
饿汉式实例创建开销小
懒汉式不推荐
同步方法性能要求不高
双重检查锁性能要求高
静态内部类大多数场景
枚举最佳实践

练习

  1. 实现一个线程安全的单例配置管理器
  2. 使用枚举实现一个单例,包含多个方法
  3. 分析为什么双重检查锁需要 volatile 关键字
  4. 实现一个数据库连接池的单例模式

参考资源