← 返回题目列表

饿汉式和懒汉式单例有什么区别?

高频 简单 第 1 / 26 题 更新于 2026/07/28
单例模式饿汉式懒汉式延迟初始化

简化版

饿汉式在类初始化阶段创建实例,实现简单且天然利用类初始化的线程安全;懒汉式在首次访问时才创建,能延迟成本,但必须正确处理并发发布。选择依据是创建成本、失败处理和是否一定会使用,而不是盲目追求懒加载。

详细版

饿汉式通常写成静态字段直接初始化:

private static final Service INSTANCE = new Service();

它的优点是代码少、线程安全、获取路径无额外同步;代价是类初始化时就占用创建成本,即使最终没有使用实例也会创建。

最简单的懒汉式是在 getInstance() 中判断 null 后创建,但这种写法在并发环境下可能创建多个对象。要安全延迟初始化,可以使用同步方法、双重检查锁或静态内部类。

如果实例轻量、必然使用且初始化不易失败,优先考虑饿汉式;如果实例昂贵、依赖在稍后才具备或可能永远不用,再选择正确实现的懒加载。

完整版教学

一、区别在于初始化时机

“饿”和“懒”描述的是何时创建对象:

  • 饿汉式:所属类初始化时创建;
  • 懒汉式:第一次真正请求实例时创建。
public final class EagerService {
    private static final EagerService INSTANCE = new EagerService();
    private EagerService() {}
    public static EagerService getInstance() { return INSTANCE; }
}

Java 虚拟机负责同步类初始化,因此静态字段初始化完成后,其他线程才能正常观察到初始化结果。这里的线程安全来自语言和虚拟机的类初始化协议。

二、朴素懒汉式的问题

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

两个线程可能同时发现 instance == null,随后各自创建对象。即使最后静态字段只保留一个引用,也已经产生过多个实例,构造过程中的注册、文件创建等副作用还可能执行多次。

三、两者的工程权衡

维度饿汉式正确实现的懒汉式
创建时机类初始化阶段首次获取时
实现复杂度取决于同步方案
首次访问延迟较低需要承担创建成本
不使用时的成本仍会创建可以避免
初始化失败时机较早暴露首次访问时暴露

饿汉式也不是“程序启动就一定创建”。只有类发生主动使用并进入初始化阶段时,静态字段初始化才执行;类仅被加载或验证,并不必然已经初始化。

四、选择时关注真实成本

很多对象的创建成本很小,强行使用复杂的懒加载只会增加代码风险。只有在对象创建昂贵、依赖外部资源、使用概率低,或者需要推迟到某些配置就绪之后时,懒加载才具有明确收益。

如果对象初始化需要 I/O,还应考虑失败重试、超时和资源关闭。单例模式本身不会替你解决这些生命周期问题。

五、把“类初始化”和“首次业务调用”放到时间线上

饿汉式并不等于 JVM 启动瞬间创建,它发生在所属类首次主动使用并进入初始化时;懒汉式把创建进一步推迟到获取方法第一次真正需要实例时。两者的差异最终落在成本由谁承担、失败何时暴露以及并发控制由谁完成。

t0: JVM 启动,Service 尚未主动使用
t1: 读取 EagerService.INSTANCE -> 类初始化并创建
t2: 后续获取仅返回现有引用
另一方案 t1: 仅加载 LazyService,不创建对象
t2: 首次 getInstance() -> 执行并发初始化协议
t3: 首次调用承担 120 ms 假设创建延迟
若从未调用,懒汉式避免这 120 ms 和资源占用

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

六、保证范围与失效边界

评审维度本题结论
唯一性/正确性保证饿汉式依赖类初始化协议;懒汉式只有采用正确同步方案才保证一次创建与安全发布。
作用域边界初始化时机位于单个类加载边界内,不决定跨进程唯一性。
仍未解决的问题昂贵 I/O 放进静态初始化可能导致类初始化失败难重试;错误懒加载会产生竞态。
更合适的替代方案轻量且必用选饿汉;昂贵且可能不用选静态内部类、容器或显式可重试初始化器。

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

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

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

记忆钩子:饿汉省并发心智,懒汉省未使用成本;先量化成本,再决定是否值得延迟。

八、常见误区与追问

  • 误区:饿汉式一定在应用进程启动时创建。 只有类发生主动使用并初始化时才执行静态字段初始化。
  • 误区:懒汉式天然更省内存。 实例一旦创建占用相同,收益只存在于可能永不使用或推迟占用的场景。
  • 误区:懒加载只写 null 判断就够了。 并发线程可能同时观察 null 并各自构造。
  • 追问:初始化依赖网络且允许重试怎么办? 类初始化失败语义不灵活,应使用显式工厂、Future 或容器生命周期管理。
  • 追问:首次访问延迟如何处理? 可在明确生命周期阶段预热,或接受延迟并配置超时与监控。
  • 追问:饿汉式的 final 字段有什么价值? 静态 final 引用初始化后不可重新指向其他实例,进一步收紧赋值入口。

九、加强记忆

饿汉式用更早的创建换简单可靠,懒汉式用更复杂的并发控制换延迟成本。面试中先说初始化时机,再比较线程安全、首次延迟和失败时机,最后根据对象是否昂贵且是否必用作选择。