真实面经题目 · 原创解析
线程安全的单例模式怎么写?
这题考察 Java 单例在并发下的安全发布、延迟初始化、重排序风险,以及枚举、静态内部类、DCL 各自的边界。
真实面经题目 · 原创解析
这题考察 Java 单例在并发下的安全发布、延迟初始化、重排序风险,以及枚举、静态内部类、DCL 各自的边界。
线程安全单例常见三种回答:枚举最简洁,天然处理序列化并且更难被反射破坏;静态内部类利用类加载初始化的线程安全性,既懒加载又不需要显式加锁;双重检查锁 DCL 需要 `private static volatile` 实例字段,先判空、加锁、锁内再判空,避免对象构造和引用赋值重排序导致其他线程看到半初始化对象。面试中推荐优先写枚举或静态内部类,再说明 DCL 的 `volatile` 原因和反射、序列化、类加载器隔离等边界。
如果没有特殊要求,枚举单例最稳;需要懒加载且希望写成普通类时,用静态内部类 holder;只有被要求说明并发细节时,再写 DCL。
外部类加载时不会初始化 holder,第一次调用 `getInstance` 才触发 holder 初始化。JVM 对类初始化加锁,保证 `INSTANCE` 只创建一次并安全发布。
第一次判空避免每次都加锁;锁内第二次判空防止多个线程排队进入后重复创建。少任意一次都会影响性能或正确性。
创建对象大致包含分配内存、初始化字段、把引用赋给变量。没有 `volatile` 时,引用赋值可能被重排到初始化完成前,其他线程看到非空引用却读到未初始化状态。
反射可能调用私有构造器,反序列化可能创建新对象,多个 classloader 也可能各自加载一份类。枚举能更自然地抵抗序列化问题,但 classloader 隔离仍是边界。
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;
}
} 为了禁止实例引用赋值和对象初始化之间的危险重排序,并保证写入对其他线程可见。
Holder 类只有在第一次调用 `getInstance` 并访问 `Holder.INSTANCE` 时才初始化,外部类加载不会创建实例。
代码短,JVM 保证枚举实例唯一,天然处理序列化创建新对象的问题,也更难通过普通反射破坏。
不一定。不同 classloader 可以加载出不同 Class 对象,从而各自持有单例。分布式系统里每个进程也会有自己的单例。