真实面经题目 · 原创解析

如何实现 Java 单例模式?

Java 单例模式的目标是保证一个类在进程内只创建一个实例,并提供全局访问点。实现方式包括饿汉式、同步懒汉式、双重检查锁、静态内部类和枚举。面试中要重点说明线程安全、延迟加载、性能、反射和序列化对单例唯一性的影响。

出现于:字节跳动 · 客户端

60 秒回答模板

常见实现可以按推荐程度回答:简单场景用枚举单例,天然防反射破坏并支持序列化;需要延迟加载且保持类形式时,用静态内部类 Holder,依赖类加载初始化的线程安全保证;如果必须手写懒加载,可以用 volatile 加双重检查锁,第一次判断减少锁开销,synchronized 内第二次判断防止重复创建,volatile 防止指令重排导致半初始化对象被看到。饿汉式简单安全但不懒加载;普通懒汉式如果没有同步,在多线程下可能创建多个实例。

考点 线程安全
难度 真实面经高频题
回答目标 讲清机制、边界和追问

深入解析

01

单例目标

单例解决的是实例数量和访问入口问题:某些对象代表全局配置、资源管理器、注册中心或无状态服务,希望进程内只有一个实例。它不只是把构造器写成 private,还要处理类加载、并发创建、序列化反序列化、反射调用构造器以及测试可替换性等边界。

02

饿汉式

饿汉式在类初始化阶段直接创建实例,通常通过 private static final 字段保存。它依赖 JVM 类初始化的线程安全语义,代码简单、没有并发创建问题。缺点是类一加载就创建对象,如果实例很重或未必使用,会浪费启动资源。它适合轻量、必然使用、初始化失败可以尽早暴露的对象。

03

双重检查锁

双重检查锁用于懒加载并降低每次访问都加锁的成本。外层 if 判断实例是否存在,存在则直接返回;不存在时进入 synchronized,内层 if 再次确认,避免多个线程排队进入后重复创建。instance 必须声明为 volatile,否则对象分配、构造和引用赋值的重排可能让其他线程看到未完全初始化的对象。

04

静态内部类

静态内部类 Holder 是更优雅的懒加载方案。外部类加载时不会立刻初始化 Holder,只有调用 getInstance 并访问 Holder.INSTANCE 时,Holder 才被类加载器初始化。类初始化过程由 JVM 保证线程安全,因此不需要手写 synchronized 和 volatile,既懒加载又简洁,常用于普通类单例。

05

枚举单例

枚举单例通常是最强健的方式。它语法简单,JVM 对枚举实例创建有特殊约束,能抵御常规反射创建新实例,也天然处理序列化返回同一枚举常量的问题。缺点是形式不如普通类灵活,例如不适合继承其他类,也不适合需要延迟到复杂参数传入后才初始化的场景。

易错点

  • 写普通懒汉式但不加同步,忽略并发首次访问会创建多个实例。
  • 写双重检查锁却漏掉 volatile,无法保证安全发布和禁止半初始化对象被读取。
  • 认为 private 构造器就绝对安全,忽略反射和反序列化可能破坏唯一性。
  • 把单例当成万能全局变量,忽视全局可变状态对测试、并发和模块边界的影响。

面试官追问

双重检查锁为什么必须加 volatile?

new 对象不是单一原子动作,可能发生分配内存、引用赋值、执行构造之间的重排。volatile 禁止相关重排并保证可见性,避免其他线程拿到半初始化实例。

静态内部类为什么能懒加载?

Holder 类只有在首次主动使用时才会初始化。调用外部类其他静态方法不会创建实例,直到访问 Holder.INSTANCE,JVM 才执行 Holder 的类初始化流程。

枚举单例有什么优势?

枚举实例由 JVM 管理,常规反射不能创建额外枚举对象,序列化也会按枚举常量恢复同一实例。它用很少代码覆盖了很多普通单例容易遗漏的边界。

Spring Bean 默认单例和手写单例一样吗?

不完全一样。Spring 默认 singleton 是容器范围内单例,由容器管理创建和注入;手写单例通常是类自身控制全局实例。多个容器或类加载器下,边界也不同。