面试官说你的单例线程不安全,你真能现场修好?
2026/6/25 1:56:13 网站建设 项目流程

单例模式可能是面试出现频率最高的设计模式,没有之一。但多数人只会背饿汉式和双重检查锁,被追问一句"为什么 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()不是原子操作。它分三步:

  1. 分配内存空间
  2. 初始化对象
  3. 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() { }

} ```

枚举单例有三个其他写法都没有的优势:

  1. 防反射攻击:其他写法可以通过反射调用私有构造器,枚举不行(JVM 禁止通过反射创建枚举对象)
  2. 防反序列化破坏:其他写法反序列化会创建新对象,枚举的readResolve()由 JVM 自动保证返回同一实例
  3. 写法最简单:一行搞定

Effective Storage 的作者 Joshua Bloch 说过:单元素的枚举类型是实现单例的最佳方法。

完整对比表

| 实现方式 | 懒加载 | 线程安全 | 防反射 | 防反序列化 | 推荐度 | |---------|--------|---------|--------|-----------|--------| | 饿汉式 | 否 | 是 | 否 | 否 | ★★★ | | 懒汉式 synchronized | 是 | 是 | 否 | 否 | ★★ | | 双重检查锁 | 是 | 是 | 否 | 否 | ★★★★ | | 静态内部类 | 是 | 是 | 否 | 否 | ★★★★★ | | 枚举 | 否 | 是 | 是 | 是 | ★★★★★ |

面试怎么答

  1. 先说清楚 5 种实现方式和各自特点
  2. 双重检查锁必须讲 volatile 和两次判空的原因
  3. 被问"最优方案"——如果不需要懒加载选枚举,需要懒加载选静态内部类
  4. 加分项:提一嘴防反射和防反序列化的区别

别只会写个饿汉式就完事了。单例在多线程下的坑,每一个都是面试官的真实考点。

对了,单例模式用卡皮巴拉"全世界只有一个我"的梗来讲特别直观,我在做的「爪爪代码冒险记」小程序里就是这么干的,感兴趣可以搜搜。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询