返回首页
📝502 ⏱️3 分钟📅2025-12-16📄Software Architecture#Java / Design Patterns / Singleton / Best Practices / Multi-threading

Java 设计模式详解:单例模式 (Singleton Pattern)

单例模式 (Singleton Pattern)

模式定义

单例模式(Singleton Pattern)是一种创建型设计模式,其核心目的是确保一个类在整个系统中只有一个实例,并提供一个全局访问点

核心三要素

  1. 唯一性:确保一个类只有一个实例。
  2. 自控性:类必须自行创建这个实例(Self-instantiation)。
  3. 全局性:必须向整个系统提供这个实例。

实现原理与规范

如何确保唯一实例?

要实现单例,必须遵循以下代码规范:

  1. 私有构造方法:使用 private 修饰构造函数,禁止外部通过 new 关键字实例化。
  2. 私有静态变量:使用 private static 成员变量持有当前类的唯一实例。
  3. 公有静态方法:提供一个 public static 方法(通常命名为 getInstance()),向外界返回该实例。

典型应用场景

单例模式通常用于管理无状态的工具类或共享资源,因为有状态的单例在多线程环境下容易产生数据竞争。

  • 序列号生成器:保证ID全局唯一。
  • Web 页面计数器:确保计数准确,不因刷新而重置。
  • 资源管理器:如 IO 读写、数据库连接池(Database Connection Pool)。
  • 日志应用:Log4j 等日志框架,保证日志文件的独占读写。
  • 配置中心:全局配置文件的读取与缓存。

单例模式的优缺点

优点

  1. 资源高效:对于重量级资源(如数据库连接、线程池),避免了频繁创建和销毁的开销。
  2. 数据一致性:全局唯一的实例可以避免多重状态导致的逻辑冲突。
  3. 避免竞争:在多线程环境下,集中管理对共享资源的访问(如写文件)。
  4. 简化访问:提供了全局访问点,降低了模块间的耦合。

潜在风险(多实例可能性)

虽然单例模式旨在保证唯一性,但在以下极端情况下可能失效:

  • 分布式环境:每个 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 实际上进行了三步操作:

  1. 分配内存 (Allocate memory)。
  2. 初始化对象 (Initialize object)。
  3. 指向地址 (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