Skip to content

单例模式 (Singleton Pattern)

什么是单例模式?

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。

适用场景

  • 配置管理:应用程序的配置信息只需要加载一次。
  • 日志记录器:整个系统共享一个日志实例。
  • 数据库连接池:避免频繁创建和销毁连接。
  • 线程池:统一管理线程资源。

实现方式

1. 饿汉式(Eager Initialization)

类加载时就创建实例,线程安全但不支持延迟加载。

java
/**
 * 饿汉式单例
 * @author yjhu
 */
public class EagerSingleton {
    private static final EagerSingleton INSTANCE = new EagerSingleton();
    
    private EagerSingleton() {}
    
    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

2. 懒汉式(Lazy Initialization)

需要时才创建实例,但需要处理线程安全问题。

java
/**
 * 懒汉式单例(双重检查锁)
 * @author yjhu
 */
public class LazySingleton {
    // volatile 防止指令重排序
    private static volatile LazySingleton instance;
    
    private LazySingleton() {}
    
    public static LazySingleton getInstance() {
        if (instance == null) {                     // 第一次检查
            synchronized (LazySingleton.class) {
                if (instance == null) {             // 第二次检查
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

为什么需要双重检查?

检查位置作用
第一次检查(synchronized 外)避免每次调用都进入同步块,提升性能。如果实例已创建,直接返回。
第二次检查(synchronized 内)防止多个线程同时通过第一次检查后,重复创建实例。

场景模拟:假设线程 A 和线程 B 同时调用 getInstance()

  1. 两者都通过第一次 if (instance == null) 检查
  2. 线程 A 先获取锁,创建实例,释放锁
  3. 线程 B 获取锁后,如果没有第二次检查,会再次创建实例 ❌

为什么需要 volatile?

instance = new LazySingleton() 这行代码并非原子操作,实际分为三步:

1. 分配内存空间
2. 初始化对象
3. 将 instance 指向分配的内存地址

由于 JVM 的指令重排序优化,实际执行顺序可能变成 1 → 3 → 2

问题场景

  1. 线程 A 执行到步骤 3(instance 已指向内存,但对象未初始化)
  2. 线程 B 进入 getInstance(),第一次检查发现 instance != null
  3. 线程 B 直接返回未初始化完成的对象 ❌

volatile 的作用

  • 禁止指令重排序,确保 1 → 2 → 3 的执行顺序
  • 保证可见性,一个线程修改后,其他线程立即可见

3. 静态内部类(推荐)

兼顾线程安全和延迟加载,是 Java 中最优雅的实现方式。

java
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

/**
 * 静态内部类单例模式
 * @author yjhu
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Singleton {

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

原理:JVM 在加载外部类时不会加载静态内部类,只有在调用 getInstance() 时才会加载 Holder 类,从而实现延迟加载。同时,类加载过程是线程安全的。

4. 枚举方式(最安全)

《Effective Java》推荐的方式,天然防止反射攻击和序列化问题。

java
/**
 * 枚举单例
 * @author yjhu
 */
public enum EnumSingleton {
    INSTANCE;
    
    public void doSomething() {
        // 业务逻辑
    }
}

各实现方式对比

实现方式线程安全延迟加载防反射攻击防序列化攻击
饿汉式
双重检查锁
静态内部类
枚举

注意事项

  1. 反射攻击:除枚举外,其他方式都可以通过反射调用私有构造函数创建新实例。
  2. 序列化问题:如果单例类实现了 Serializable,反序列化时会创建新对象,需要添加 readResolve() 方法。
  3. Spring 中的单例:Spring 的 @Scope("singleton") 是容器级别的单例,与设计模式中的单例不同。