Sunday面试指南

Java 单例模式怎么实现?双重检查锁为什么需要 volatile?

下面是一段教学用的模拟面试。

🧑‍💻 面试官:Java 懒加载单例怎么写?

🙋‍♂️ 我:可以用双重检查锁,第一次为空时进入锁,再检查一次。

🧑‍💻 面试官:为什么要检查两次?

🙋‍♂️ 我:外层判断减少已初始化后的加锁,内层判断避免多个线程重复创建。

🧑‍💻 面试官:那只加锁,不给实例引用加 volatile,为什么还不够?这个单例里放一个普通 HashMap,就线程安全了吗?

单例要分别解决「只创建一次」「安全地交给其他线程」和「对象内部怎么并发使用」。前两件做对,不代表第三件也成立。

面试速答(60 秒版)

单例模式希望在某个约定范围内共享一个实例。Java 中可以选择饿汉式、加锁的懒汉式、静态内部类或枚举等方案。

双重检查锁的外层判断,让初始化之后的访问不必每次加锁;内层判断,则防止等待锁的线程再次创建对象。

但实例引用还需要 volatile。因为外层读取没有经过同一把锁,要保证初始化结果被安全发布,不能让其他线程看到引用却无法可靠看到构造完成的状态。

实际选择时,我会优先考虑简单、容易证明正确的方案,例如利用类初始化机制的静态内部类。同时明确单例的范围:一个类加载器里的单例,不等于整个集群只有一份。实例唯一,也不等于成员变量操作线程安全。

创建、发布、并发使用,分别保证

图:创建、发布、并发使用,分别保证。

知识点详解:单例需要守住三个不同的条件

为什么“只 new 一次”还不是完整答案?

假设多个线程第一次请求配置管理器。我们希望它们使用同一个对象,首先要防止重复创建。

但对象创建后还要交给其他线程。共享引用的读取者,需要看到已经完成的初始化结果。这是安全发布的问题。

最后,配置管理器如果还会修改内部数据,多线程操作这些数据是否安全,又是另一件事。不能因为构造过程用了锁,就认为以后所有方法自动带锁。

双重检查锁,两次检查各自挡住什么?

外层检查发现实例已经存在,就直接返回,这是常用访问路径。

如果外层发现为空,线程进入同步区域。但在等待锁期间,另一个线程可能已经创建好了对象。所以拿到锁后还要再检查一次,不能直接创建。

内层检查解决的是重复初始化。volatile 解决的是同步区域外读引用的发布和可见性要求。它们不能互相代替。

常见解释会把对象创建描述成“分配空间、初始化、保存引用”。这有助于理解,但不是要求 JVM 必须按某个固定机器指令列表执行。真正应该依据的是 Java 内存模型中的同步关系,以及 volatile 写入与后续读取的 happens-before 保证,见 JLS 第 17 章。

两次检查和 volatile,各有职责

图:两次检查和 volatile,各有职责。

静态内部类为什么更容易解释?

可以让持有实例的内部类,只在第一次实际访问时初始化。类初始化过程由 JVM 协调,其他线程不会随意并行执行同一次初始化。

因此,它既能延迟创建,又不必手写双重检查锁。类初始化的触发和同步规则见 JLS 第 12 章。

当然,如果初始化失败,需要理解类初始化失败后的行为;如果构造需要运行时参数,也要重新考虑设计,而不是强行套无参数单例。

方案主要特点需要想清楚
饿汉式初始化简单是否需要提前创建
同步懒汉式容易证明互斥每次访问的同步成本
双重检查锁减少后续加锁volatile 与两次检查
静态内部类利用类初始化初始化失败及参数来源
枚举简洁且有语言支持是否适合当前生命周期

单例到底是“哪里只有一个”?

同一个类由不同类加载器加载,可以形成不同的类身份和静态状态。不同进程、不同服务实例,更不会因为写了一个静态变量就共享同一对象。

回到配置管理器,单例只能减少某个运行范围内的重复实例。跨实例一致性需要另行设计;内部可变 HashMap 也需要自己的并发方案。

本题讨论 Java 类初始化和内存模型。TypeScript 模块缓存、Python 模块对象等机制没有完全相同的保证,不提供伪等价代码。

单例要先声明唯一范围

图:这里假设两个类加载器各自定义了同一个类;如果都委托给同一个父加载器,就不能据此判断有两个实例。

面试官继续追问

final 字段就能代替 volatile 吗?

不能简单替换。final 有自己的初始化安全规则,但双重检查锁中共享的是一个需要发布的实例引用。应按整个发布方式判断,而不是看到 final 就省略同步设计。

Spring 默认单例和这里是一回事吗?

Spring 的 singleton 是容器中的 Bean 作用域,通常按容器和 Bean 定义管理。它不等于某个 Java 类从语言层面永远只能创建一个对象。

配置需要热更新怎么办?

创建一次的管理器可以管理多次更新。可以考虑构造完整的新配置快照,再安全替换引用;不要让读者看到更新了一半的共享对象。

面试速记卡

  • 单例:先声明唯一的范围。
  • 双重检查:外层减加锁,内层防重复创建。
  • volatile:保障同步区外读取所需的安全发布。
  • 静态内部类:利用 JVM 类初始化机制。
  • 共享实例:不自动保证内部可变状态线程安全。

公司面试真题

真题根据求职者公开面经整理,题意经过概括,非逐字原话或公司官方题库;本文为 Sunday 的独立解析。

浏览公司面试真题 →
简历汪永久免费在线制作简历,模板直接套用、导出无水印,永久免费、下载免费,不需要付费解锁任何功能。去写简历