单例模式:理解设计模式思维的起点

发布于:2025-09-15 ⋅ 阅读:(20) ⋅ 点赞:(0)

在软件开发中,有些对象我们只希望它存在一个实例,比如:

  • 数据库连接池
  • 日志管理器
  • 配置中心
  • 线程池

如果每次都 new 一个,不仅浪费资源,还可能导致状态混乱。这时,单例模式(Singleton Pattern) 就派上用场了。

什么是单例模式?

定义:

确保一个类只有一个实例,并提供一个全局访问点。

核心目标:

  1. 私有化构造函数 → 防止外部 new
  2. 内部创建唯一实例 → 控制实例化过程
  3. 提供静态方法获取实例 → 全局访问

单例模式的6种实现方式

饿汉式

public class Singleton {
    // 类加载时就创建实例
    private static final Singleton INSTANCE = new Singleton();

    // 私有构造函数
    private Singleton() {}

    // 提供全局访问方法
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

优点:实现简单,线程安全(类加载机制保证)
缺点:不管用不用,类加载时就创建,可能造成资源浪费

适用场景:对象创建成本低,且一定会用到。

懒汉式

“懒汉式”因其懒加载(Lazy Loading)特性——用的时候才创建实例——被广泛讨论。但“懒汉式”有一个致命问题:默认是非线程安全的!今天,我们就从最基础的懒汉式开始,一步步发现问题、分析问题、解决问题,最终写出一个高效、安全、可靠的单例实现

最基础的懒汉式
public class Singleton {
    private static Singleton instance;

    // 私有构造函数
    private Singleton() {}

    // 提供全局访问点
    public static Singleton getInstance() {
        if (instance == null) {              // 1. 判断是否已创建
            instance = new Singleton();      // 2. 创建实例
        }
        return instance;                     // 3. 返回实例
    }
}

没有则创建,有 则直接返回。但是在多线程环境下会创建多个实例

两个线程同时调用:

时间 线程 A 线程 B
t1 执行 if (instance == null) → 成立
t2 执行 if (instance == null) → 成立(A还没创建)
t3 执行 new Singleton() 执行 new Singleton()
t4 返回 A 实例 返回 B 实例

结果:两个线程拿到了不同的实例,违反了单例原则

直接使用Synchronized关键字

synchronized 保证同一时刻只有一个线程能进入该方法。线程安全问题解决。

public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}

为了保证线程安全,最直接的方法就是加锁。但是无论实例是否已经创建,在获取实例时都需要先获取锁,性能很差。那么改进思路就是 只有在没有创建实例时,才获取锁 去创建实例。

双重检查锁定

我们其实只需要在第一次创建实例时加锁,后续直接返回即可。

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;
    }
}
优点:
  • 懒加载:用时才创建
  • 高性能:只有第一次创建时加锁
  • 线程安全:双重检查 + 锁机制保证
为什么需要两次 if (instance == null)
  • 第一次:避免不必要的加锁(如果实例已存在,直接返回)
  • 第二次:防止多个线程同时通过第一层检查后,重复创建实例
为什么必须加 volatile

这是最容易被忽略的关键点!

如果不加 volatile,可能会发生指令重排序(Instruction Reordering),导致其他线程拿到一个未初始化完成的对象

问题出现在这一行

instance = new Singleton();

这行代码实际上分为三步:

  1. 分配内存空间
  2. 调用构造函数初始化对象
  3. 将 instance 指向内存地址

JVM 可能会重排序为:1 → 3 → 2

如果线程 A 执行到第 3 步(instance 已指向内存),但构造函数还没执行完,此时线程 B 调用 getInstance(),发现 instance != null,直接返回——拿到一个半初始化的对象!

防止反射攻击

我们已经通过 双重检查锁定 + volatile 实现了一个线程安全、懒加载的单例模式,看起来“完美”了。但在某些极端场景下,单例仍然可能被破坏——比如,有人通过 反射(Reflection) 强行调用私有构造函数!虽然这在正常业务中很少见,但如果你的系统面临安全风险(如代码注入、反序列化漏洞等),就必须考虑这种可能性。

让我们来模拟一下攻击过程:

// 恶意代码:通过反射破坏单例
public class ReflectionAttack {
    public static void main(String[] args) throws Exception {
        // 正常获取实例
        Singleton s1 = Singleton.getInstance();

        // 通过反射获取构造函数
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true); // 暴力破解私有权限

        // 强行创建新实例
        Singleton s2 = constructor.newInstance();

        // 输出结果
        System.out.println("s1 == s2? " + (s1 == s2)); 
        // 输出:false 两个不同的实例!
    }
}

看!尽管构造函数是 private 的,但反射依然可以绕过访问控制,创建出第二个实例,彻底破坏了单例的唯一性

 如何防御?—— 在构造函数中加判断

private Singleton() {
    // 防御反射攻击
    if (instance != null) {
        throw new RuntimeException("单例模式已被破坏,禁止重复创建!");
    }
    // 正常初始化...
}

这样,当反射试图第二次创建实例时,构造函数会检测到 instance 已存在,直接抛出异常,阻止攻击。

最终的线程安全+懒加载+放破坏

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;
    }

    // 防止序列化破坏
    private Object readResolve() {
        return instance;
    }
}

如果你希望一劳永逸地防止反射攻击,推荐使用 枚举实现单例

枚举实现单例模式https://blog.csdn.net/2402_87283759/article/details/151622371?spm=1001.2014.3001.5502