60 秒回答模板

线程安全单例常见三种回答:枚举最简洁,天然处理序列化并且更难被反射破坏;静态内部类利用类加载初始化的线程安全性,既懒加载又不需要显式加锁;双重检查锁 DCL 需要 `private static volatile` 实例字段,先判空、加锁、锁内再判空,避免对象构造和引用赋值重排序导致其他线程看到半初始化对象。面试中推荐优先写枚举或静态内部类,再说明 DCL 的 `volatile` 原因和反射、序列化、类加载器隔离等边界。

考点 核心机制与工程取舍
难度 中高频面试题
回答目标 按定义、机制、场景讲清楚

深入解析

01

推荐优先级要明确

如果没有特殊要求,枚举单例最稳;需要懒加载且希望写成普通类时,用静态内部类 holder;只有被要求说明并发细节时,再写 DCL。

02

静态内部类靠类初始化保证安全

外部类加载时不会初始化 holder,第一次调用 `getInstance` 才触发 holder 初始化。JVM 对类初始化加锁,保证 `INSTANCE` 只创建一次并安全发布。

03

DCL 必须有两次判空

第一次判空避免每次都加锁;锁内第二次判空防止多个线程排队进入后重复创建。少任意一次都会影响性能或正确性。

04

`volatile` 防止半初始化可见

创建对象大致包含分配内存、初始化字段、把引用赋给变量。没有 `volatile` 时,引用赋值可能被重排到初始化完成前,其他线程看到非空引用却读到未初始化状态。

05

单例边界不要说绝对

反射可能调用私有构造器,反序列化可能创建新对象,多个 classloader 也可能各自加载一份类。枚举能更自然地抵抗序列化问题,但 classloader 隔离仍是边界。

java

静态内部类与 DCL 写法

final class HolderSingleton {
  private HolderSingleton() {}

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

  static HolderSingleton getInstance() {
    return Holder.INSTANCE;
  }
}

final class DclSingleton {
  private static volatile DclSingleton instance;

  private DclSingleton() {}

  static DclSingleton getInstance() {
    if (instance == null) {
      synchronized (DclSingleton.class) {
        if (instance == null) {
          instance = new DclSingleton();
        }
      }
    }
    return instance;
  }
}
  • 静态内部类适合作为首选手写答案;DCL 用来解释双重判空和 volatile 的并发语义。枚举写法更短,但这里保留普通类代码方便现场讲解。

易错点

  • 懒汉式只判空不加同步,并发下会创建多个实例。
  • DCL 忘记 `volatile`,可能出现半初始化对象被读取。
  • 锁内不做第二次判空,排队线程会重复创建。
  • 把单例说成绝对不可破坏,忽略反射、序列化和 classloader 边界。

面试官追问

DCL 为什么一定要 `volatile`?

为了禁止实例引用赋值和对象初始化之间的危险重排序,并保证写入对其他线程可见。

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

Holder 类只有在第一次调用 `getInstance` 并访问 `Holder.INSTANCE` 时才初始化,外部类加载不会创建实例。

枚举单例有什么优点?

代码短,JVM 保证枚举实例唯一,天然处理序列化创建新对象的问题,也更难通过普通反射破坏。

单例一定全 JVM 唯一吗?

不一定。不同 classloader 可以加载出不同 Class 对象,从而各自持有单例。分布式系统里每个进程也会有自己的单例。