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

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

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

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

相关推荐
·薯条大王7 小时前
经济实惠玩云服务器|一台云服务器多人共用,子账号配置教程
java·linux·运维·服务器·汇编·c++·python
ZJH__GO10 小时前
网络编程v4pro--实现聊天室文件传输功能
java·服务器·网络·计算机网络
程序员黑豆10 小时前
Windows 系统 Java 环境变量配置全攻略:解决“不是内部或外部命令”
java·前端·ai编程
命运之光10 小时前
【C语言完整代码】就诊信息管理系统
java·c语言·开发语言
凤山老林10 小时前
Spring Boot @Async 线上实战:从默认配置到生产级线程池治理
java·spring boot·后端
marvelyu11 小时前
每天10分钟学会OceanBase系列(Day 20):跨机房容灾实战——构建多数据中心高可用架构
java·大数据·数据库
AI人工智能+电脑小能手12 小时前
大白话说Java设计模式-04-工厂方法模式(业务实战篇)
java·设计模式·工厂方法模式·架构设计·代码解耦
BUG指挥官12 小时前
Sa-Token和Spring Security对比
java·后端·spring
小当家.10513 小时前
深入理解 ReAct Agent:从原理到 Java 实战
java·react.js·agent·react·架构设计·agent设计
geminigoth13 小时前
Spring AI Alibaba 入门开发一(备份)
java·人工智能·spring