← 返回题目列表

什么是单例模式?它适合哪些场景?

高频 简单 第 2 / 26 题 更新于 2026/07/28
单例模式创建型模式全局访问点设计原则

简化版

单例模式保证一个类在约定的作用域内只有一个实例,并提供统一的获取入口。它适合确实需要共享同一份状态或资源的对象,但不应把单例当成方便访问的全局变量。

详细版

单例模式通常包含三个要点:

  1. 构造方法私有化,限制外部直接创建对象;
  2. 类内部保存唯一实例;
  3. 对外提供获取实例的方法。

常见场景包括配置中心客户端、进程内注册表、ID 生成器和需要集中协调的资源管理器。使用前要确认“唯一”的边界:传统 Java 单例通常是每个定义它的 ClassLoader 一份,而不是天然覆盖整个分布式系统。

单例的优点是避免重复创建、集中管理共享资源;缺点是隐藏依赖、共享可变状态可能引发并发问题,而且全局生命周期会增加测试隔离和资源释放的难度。无状态服务更适合交给依赖注入容器管理,业务数据通常不适合放进单例。

完整版教学

一、单例解决的核心问题

单例解决的不是“少写几次 new”,而是对象身份必须统一的问题。如果两个组件必须访问同一个注册表、同一个进程级协调器或同一份缓存索引,就需要明确谁创建它、谁持有它以及其他代码如何获取它。

典型结构如下:

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

    private AppRegistry() {}

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

private 构造方法控制创建入口,static 字段保存共享实例,getInstance() 提供访问点。具体实现还要继续考虑初始化时机、线程安全、序列化和反射等问题。

二、“只有一个”的边界必须说清楚

单例并不自动等于全世界唯一:

  • 同一 Java 类被不同 ClassLoader 定义时,可以各自拥有静态字段和实例;
  • 同一应用启动两个进程时,每个进程都有自己的单例;
  • 分布式部署多个节点时,每个节点也会有独立实例;
  • Spring 的默认单例是每个容器、每个 Bean 定义一个实例。

如果业务要求跨进程唯一,例如全局递增序号或分布式主节点,就需要数据库唯一约束、分布式锁或一致性协议,不能只靠进程内单例。

三、适用与不适用场景

适用的前提是对象确实代表共享资源或协调角色,例如昂贵且可复用的客户端、应用级配置快照、进程内注册中心。仅仅因为“很多地方都要调用”并不足以使用单例,依赖注入同样可以方便地共享实例,同时让依赖关系更清晰。

下面几类对象通常不适合做单例:

  • 保存用户、请求或会话状态的对象;
  • 生命周期需要精确释放且难以跟随进程结束的资源;
  • 为了省事而塞入大量可变业务数据的“万能管理器”;
  • 测试中需要频繁替换或并行隔离的依赖。

四、单例不等于线程安全

安全地创建出唯一实例,只解决了初始化问题。如果实例内部有可变字段,多个线程仍可能同时修改它。实现时还要采用不可变状态、线程安全集合、锁或其他同步机制保护业务操作。

“实例唯一”和“实例内所有方法线程安全”是两件事,面试中应主动区分。

五、先用资源身份判断是否真的需要单例

假设进程内有 20 个业务组件都要向同一个注册表登记处理器。如果每个组件各建一个注册表,登记结果彼此不可见;单例可以统一身份。但若对象只是无状态格式化器,容器共享实例就够了,不必把静态全局入口写进类。

20 个组件启动
所有组件依赖 Registry 抽象
组合根创建/取得一个进程内 Registry 实例
把同一引用注入 20 个组件
组件 A 注册 handler-x
组件 B 查询时能看到 handler-x
跨进程部署后,每个进程仍有自己的 Registry

这条时序必须同时检查“谁负责创建”和“其他线程或调用方从哪里取得实例”。只看到最终引用相同还不够;若构造副作用执行了两次、发布了半初始化状态,或者作用域边界之外又创建了一份,就已经违反了本题讨论的保证。

六、保证范围与失效边界

评审维度本题结论
唯一性/正确性保证在约定作用域内控制创建入口并提供同一对象身份。
作用域边界传统 Java 静态单例通常是每个定义 ClassLoader 一份;容器单例则按容器与定义计算。
仍未解决的问题隐藏依赖、全局可变状态、测试污染、资源释放和跨节点一致性都不会自动解决。
更合适的替代方案优先显式依赖注入;确有固定语言级身份且生命周期简单时再使用静态或枚举单例。

设计模式的保证必须带作用域。把“某个类初始化边界只创建一次”说成“整个系统永远只有一个”,或把“安全发布引用”说成“对象所有方法都线程安全”,都会把不同层次的问题混在一起。

七、工程落地时的验证方法

  1. 为构造过程增加可观察计数,使用 32 个并发任务同时获取实例,构造计数应符合本题保证。
  2. 比较引用身份时使用 ==,不要让重写后的 equals() 掩盖实际存在的多个对象。
  3. 若涉及类加载器,记录 instance.getClass().getClassLoader(),同时比较类对象和加载器身份。
  4. 若涉及序列化,至少执行一次“序列化 → 反序列化”,并核对返回引用及外部副作用。
  5. 若涉及 Spring,分别检查 Bean 名称、ApplicationContext 身份和作用域,而不只检查 Java 类名。
  6. 对共享可变字段另做并发测试;实例创建正确并不能替代业务状态的原子性测试。
  7. 对初始化失败、重复调用和资源关闭分别设计用例,确认生命周期语义可解释。

记忆钩子:单例解决的是“必须共享同一个身份”,不是“到处调用更方便”。

八、常见误区与追问

  • 误区:只要对象创建成本高就必须做单例。 对象池、容器作用域或缓存也能复用,需结合并发和生命周期选择。
  • 误区:单例就是全局变量的无害包装。 静态访问仍会隐藏依赖并放大共享状态影响面。
  • 误区:单例天然覆盖整个分布式系统。 每个进程都有独立内存和静态字段。
  • 追问:单例与静态工具类有什么区别? 单例是对象,可实现接口、持有生命周期并被注入;静态工具主要提供类级无状态函数。
  • 追问:为什么单例会降低测试隔离? 全局实例状态可能跨测试残留,且静态入口难替换。
  • 追问:哪些对象明显不适合单例? 用户会话、请求上下文和包含未同步可变业务状态的对象通常不适合。

九、加强记忆

理解单例要抓住“受控创建、统一入口、明确边界”三个关键词。先判断对象是否真的需要共享身份,再讨论懒加载和线程安全;只要边界扩展到多类加载器、多进程或多节点,就必须引入比单例更强的协调机制。