面试官说你的单例线程不安全,你真能现场修好?

单例模式可能是面试出现频率最高的设计模式,没有之一。但多数人只会背饿汉式和双重检查锁,被追问一句"为什么 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. 加分项:提一嘴防反射和防反序列化的区别

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

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

相关推荐
码云骑士1 小时前
【Java基础】JDK安装常见问题教辅-从踩坑到排雷
java·开发语言
Sunia1 小时前
《AgentX 专栏》09-MCP协议双向打通:让AgentX既能被Claude调用又能调度全球工具生态
java·架构
wyu729611 小时前
SpringBoot八股的一些概念笔记
java·面试
一只积极向上的小咸鱼1 小时前
TOML、JSON、YAML、INI 配置文件格式总结
java·服务器·json
莫逸风1 小时前
【AgentScope】4.会话(Session)详解
java·llm·agent·agentscope
吴阿福|一人公司1 小时前
类变量和实例变量的命名规范有哪些具体的例子?
java·开发语言
eddietao1 小时前
什么是 fail-fast?什么是 fail-safe?
java·面试
程序员小羊!1 小时前
05 JAVA面向对象
java·开发语言
MrJson-架构师1 小时前
AgentScope Java 2.0:打造分布式、企业级智能体底座
java·开发语言·分布式