单例模式可能是面试出现频率最高的设计模式,没有之一。但多数人只会背饿汉式和双重检查锁,被追问一句"为什么 volatile""为什么两次判空"就懵了。

更惨的是,有些人在面试里写的单例代码本身就是错的——不是忘了 volatile,就是双重检查锁写成了单次检查。这篇文章把单例在多线程下的所有坑和修法讲清楚,面试再被问到,你可以反过来问面试官。
public class HungrySingleton {
private static final HungrySingleton INSTANCE = new HungrySingleton(); private HungrySingleton() {} public static HungrySingleton getInstance() {
return INSTANCE;
}
}
饿汉式不需要任何同步,因为 INSTANCE 在类加载时就初始化了。JVM 的类加载机制保证了初始化只执行一次。
但问题也在这里:不管你用不用,实例都已经创建了。如果这个对象很重(比如持有大缓存、建立了连接池),就是浪费资源。
public class LazySingleton {
private static LazySingleton instance; private LazySingleton() {} public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton(); // 危险!
}
return instance;
}
}
两个线程同时判空通过,都进到 new 那行——创建了两个实例。
你可能会想:加个 synchronized 不就行了?
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
能保证线程安全,但每次调用都要获取锁,哪怕是实例已经创建好了。高并发下这就是性能瓶颈。
public class DCLSingleton {
private static volatile DCLSingleton instance; private DCLSingleton() {} public static DCLSingleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (DCLSingleton.class) {
if (instance == null) { // 第二次检查
instance = new DCLSingleton();
}
}
}
return instance;
}
}
第一次检查:如果实例已存在,直接返回,不走同步块。这是性能优化的关键。
第二次检查:进入同步块后再次判空。因为可能有线程 A 和 B 同时通过了第一次检查,A 先拿到锁创建了实例,B 拿到锁后如果不再检查就会再创建一个。
这是面试最容易追问的点:为什么必须加 volatile?
因为 new DCLSingleton() 不是原子操作。它分三步:
instance 引用指向分配的内存地址JVM 可能会重排序 2 和 3。如果线程 A 执行了 1→3(还没执行 2),线程 B 此时做第一次检查发现 instance != null,直接返回了一个还没初始化完成的对象——用了就崩。
volatile 的作用是禁止指令重排序,保证 new 操作的 3 个步骤按 1→2→3 执行。
public class InnerClassSingleton {
private InnerClassSingleton() {} private static class Holder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
} public static InnerClassSingleton getInstance() {
return Holder.INSTANCE;
}
}
这个写法兼顾了懒加载和线程安全:
Holder 类在 getInstance() 首次调用时才加载,INSTANCE 才初始化这是实际开发中最推荐的单例写法。
public enum EnumSingleton {
INSTANCE; public void doSomething() { }
}
枚举单例有三个其他写法都没有的优势:
readResolve() 由 JVM 自动保证返回同一实例Effective Storage 的作者 Joshua Bloch 说过:单元素的枚举类型是实现单例的最佳方法。
| 实现方式 | 懒加载 | 线程安全 | 防反射 | 防反序列化 | 推荐度 |
|---|---|---|---|---|---|
| 饿汉式 | 否 | 是 | 否 | 否 | |
| 懒汉式 synchronized | 是 | 是 | 否 | 否 | |
| 双重检查锁 | 是 | 是 | 否 | 否 | |
| 静态内部类 | 是 | 是 | 否 | 否 | |
| 枚举 | 否 | 是 | 是 | 是 |
别只会写个饿汉式就完事了。单例在多线程下的坑,每一个都是面试官的真实考点。
对了,单例模式用卡皮巴拉"全世界只有一个我"的梗来讲特别直观,我在做的「爪爪代码冒险记」小程序里就是这么干的,感兴趣可以搜搜。