前言

在软件开发中,“某个类只应该有一个实例,并且提供全局访问点”是非常常见的需求。单例模式就是为了解决这个问题:保证对象唯一,并且统一入口访问该对象。
在这里插入图片描述


1. 单例模式是什么?

单例模式:确保一个类只有一个实例,并提供一个全局访问点来访问该实例。

常见用途:

  • 配置管理类(读取配置、全局生效)
  • 线程池/缓存管理器
  • 日志器(Logger)
  • 数据库连接池(通常已有更专业实现,但思路类似)

2. 为什么要用单例?能解决什么问题?

单例模式通常用于以下场景:

  1. 控制资源数量:比如某些昂贵资源(线程池、缓存、硬件设备等)不应该重复创建。
  2. 共享状态/统一管理:需要全局统一的管理入口。
  3. 避免重复初始化:系统启动时初始化一次即可,后续复用。

3. 单例模式的实现思路

从实现方式看,常见单例分为三大类:

  1. 饿汉式(类加载时就创建)
  2. 懒汉式(第一次使用才创建)
  3. 线程安全懒汉式(如:双重检查锁 DCL、静态内部类等)

此外还有:

  • 枚举单例(Java 中最推荐之一的写法,天然防反序列化破坏)
  • 注册式/容器式单例(更灵活,但不属于经典 GoF 单例)

4. 饿汉式单例(线程安全,创建早)

4.1 代码示例(核心思想:类加载即初始化)

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

    private Singleton() {}

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

4.2 优缺点

优点

  • 天然线程安全:JVM 类加载机制保证初始化过程是安全的
  • 实现简单

缺点

  • 如果程序运行中从未用到这个单例,会造成不必要的资源占用
  • 可能导致类加载变慢(在某些场景)

适用场景

  • 单例创建成本不高
  • 或者几乎确定程序会用到该单例

5. 懒汉式单例(延迟创建,但线程不安全)

5.1 代码示例(核心思想:第一次调用才创建)

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

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

5.2 为什么线程不安全?

在多线程环境下可能出现:

  • 线程 A 判断 instance == null,准备创建
  • 线程 B 也判断 instance == null,也创建
  • 最终产生多个实例

因此:懒汉式要想正确,就必须处理并发。


6. 懒汉式 + synchronized(线程安全,但性能差)

6.1 代码示例(方法级锁)

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

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

6.2 优缺点

优点

  • 线程安全,逻辑清晰

缺点

  • 每次调用 getInstance() 都要获得锁,性能较差
  • 高并发下锁竞争明显

适用场景

  • 并发不高、对性能要求不高的场景(但一般不推荐)

7. 双重检查锁(Double-Checked Locking, DCL)

7.1 代码示例(核心思想:减少同步开销)

public class Singleton {
    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;
    }
}

7.2 为什么要 volatile?

关键点在于指令重排问题:

  • 没加 volatile 时,可能发生这样的顺序:
    1. 分配内存
    2. 将内存地址赋给 instance
    3. 调用构造函数初始化对象

此时另一个线程可能看到 instance != null,但对象还没初始化完成。

volatile 能禁止关键重排,并保证可见性。

详细了解volatile相关可以看我的另一篇文章 volatile的三大特性、底层原理

7.3 优缺点

优点

  • 懒加载
  • 高并发下性能比 synchronized 方案好

缺点

  • 代码更复杂
  • 容易漏掉 volatile

适用场景

  • 需要懒加载 + 高并发性能

8. 静态内部类单例(懒加载 + 线程安全)

8.1 代码示例

public class Singleton {
    private Singleton() {}

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

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

8.2 为什么它线程安全?

  • Holder 类只有在第一次调用 getInstance() 时才会被加载
  • JVM 类加载过程是线程安全的
  • 因此能保证 INSTANCE 只初始化一次

8.3 优缺点

优点

  • 懒加载
  • 线程安全
  • 代码简洁,不需要 synchronized / volatile

缺点

  • 初学者可能不理解“静态内部类触发加载”的机制

适用场景

  • 通常工程里这是非常推荐的实现方式

9. 枚举单例(防止“反射/序列化攻击”的方式)

9.1 代码示例

public enum Singleton {
    INSTANCE;
    public void someMethod() {}
}

9.2 优点

  • 天然支持线程安全
  • 防止反射创建新实例(enum 的机制更可靠)
  • 防止反序列化破坏单例(JVM 语义保证)

9.3 缺点

  • 不能继承其他类(enum 本身限制)
  • 用法风格不同于常规类

适用场景

  • 用于“必须绝对唯一”的单例场景

10. 单例模式的注意事项

  1. 反射破坏单例

    • 普通单例(尤其懒汉式/饿汉式)可能被反射绕过
    • 枚举单例更安全
  2. 序列化破坏单例

    • 如果实现了 Serializable,需要考虑 readResolve() 保证反序列化返回同一个实例
    • enum 单例可避免很多问题
  3. 多线程可见性与指令重排(DCL)

    • DCL 必须加 volatile(这是最常见错误)
  4. 单例不是越多越好

    • 单例会带来全局状态,滥用可能引发可维护性、测试困难、隐式耦合
  5. 是否真的需要单例?

    • 很多场景其实是“全局可访问的对象”,但不一定要严格“唯一实例”
    • 例如依赖注入(DI)容器常常能更优雅地解决“单实例需求”

11. 单例模式的适用场景总结

适合:

  • 需要全局唯一实例的场景
  • 资源初始化成本高且只需要一次
  • 工具类/管理类(配置、日志、缓存等)

不适合:

  • 业务上强依赖可变全局状态(容易造成耦合)
  • 需要频繁创建销毁、区分上下文的对象(这种不应该用单例)

12. 一句话总结:怎么选实现方式?

  • 想简单:饿汉式
  • 想懒加载又要简洁:静态内部类
  • 想懒加载 + 高并发 + 熟悉细节:双重检查锁(DCL)+ volatile
  • 强安全要求(反射/序列化):枚举单例
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐