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