🔥 单例模式终极指南:从实现到面试,一文搞定所有坑

157 阅读4分钟

单例模式是 Java 开发中最基础也最容易被面试的设计模式之一。看似简单的 "确保类只有一个实例",背后却隐藏着线程安全、反射攻击、序列化破坏等诸多陷阱。本文将用最精简的方式,带你彻底吃透单例模式的核心要点,不仅能解决开发中的实际问题,更能在面试中脱颖而出。

📌 单例模式核心概念:为什么它如此重要?

定义:确保一个类在全局范围内只有一个实例,并提供统一的访问入口。属于创建型设计模式。

核心目标

  • 资源优化:避免重复创建实例(如数据库连接池、线程池),减少内存开销。

  • 全局可控:提供唯一入口,方便管理全局状态(如 Spring 容器中的 Bean)。

现实场景类比

  • 公司 CEO 办公室:全公司只有一个,所有人通过固定入口访问。

  • 打印机管理:多线程环境下多个程序访问打印机,需确保唯一实例避免冲突。

🚀 5 种经典实现方式:从入门到精通

1. 饿汉式单例:简单但 "贪吃" 的实现

// 类加载时直接初始化实例,线程安全

public class HungrySingleton {
    private static final HungrySingleton INSTANCE = new HungrySingleton();
    private HungrySingleton() {}
    public static HungrySingleton getInstance() {
        return INSTANCE;
    }
}

优点:简单直观,类加载阶段由 JVM 保证线程安全。

缺点:无论是否使用都会提前创建实例,可能造成内存浪费("占着茅坑不拉屎")。

2. 懒汉式单例:迟到但不安全的实现

// 第一次使用时才初始化,多线程下不安全

public class LazySingleton {
    private static LazySingleton instance;
    private LazySingleton() {}
    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
         }
        return instance;
     }
}

面试题陷阱

问:为什么懒汉式在多线程下不安全?

答:当两个线程同时判断instance == null时,可能各自创建实例,导致单例失效。可通过 IDEA 的 Thread 模式调试验证。

3. 双重检查锁(DCL):高效且安全的终极方案

public class DoubleCheckSingleton {

    private volatile static DoubleCheckSingleton instance;
    private DoubleCheckSingleton() {}
    public static DoubleCheckSingleton getInstance() {
        if (instance == null) {  // 第一次检查
            synchronized (DoubleCheckSingleton.class) {
                if (instance == null) {  // 第二次检查
                    instance = new DoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

核心细节

  • volatile禁止指令重排,避免初始化未完成就被访问。

  • 两次null检查:先跳过已初始化的情况,减少锁竞争开销。

4. 静态内部类:优雅的 "懒加载" 实现

public class InnerClassSingleton {
    private InnerClassSingleton() {}
    // 内部类延迟加载,调用时才初始化
    private static class Holder {
    private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();

    }
    public static InnerClassSingleton getInstance() {
        return Holder.INSTANCE;
    }
}

优势

  • 利用类加载机制保证线程安全,无需手动加锁。

  • 避免饿汉式的内存浪费,实现真正的 "懒加载"。

5. 枚举式单例:《Effective Java》推荐的终极方案

public enum EnumSingleton {

    INSTANCE;
    // 可添加自定义方法和属性
    private Object data;
    public Object getData() { return data; }
    public void setData(Object data) { this.data = data; }
    }

面试必问题

问:为什么枚举式单例被称为最优实现?

答:

  1. 天然线程安全:枚举实例由 JVM 保证唯一。

  2. 自动防御反射:JDK 禁止通过反射创建枚举实例。

  3. 序列化安全:反序列化时直接返回已有实例,无需readResolve方法。

⚙️ 单例模式的 "致命威胁" 与解决方案

1. 反射破坏:如何阻止 "暴力创建"?

破坏代码

// 通过反射调用私有构造方法

Class\<?> clazz = InnerClassSingleton.class;
Constructor\<?> c = clazz.getDeclaredConstructor();
c.setAccessible(true);
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.println(o1 == o2);  // 输出false,单例被破坏

解决方案:在构造方法中添加校验:

private InnerClassSingleton() {
    if (Holder.INSTANCE != null) {
        throw new RuntimeException("禁止创建多个实例");
    }
}

2. 序列化破坏:如何保证反序列化后实例唯一?

破坏现象:序列化后反序列化会重新创建对象。

解决方案:添加readResolve方法:

public class SerializableSingleton implements Serializable {
    public static final SerializableSingleton INSTANCE = new SerializableSingleton();
    private SerializableSingleton() {}
    // 关键方法:反序列化时返回已有实例
    private Object readResolve() {
        return INSTANCE;
    }
}

💡 面试高频问题与满分回答

问:单例模式在 Spring 中如何应用?

:Spring 容器默认使用单例模式管理 Bean,通过ConcurrentHashMap存储 Bean 实例,保证全局唯一。例如ApplicationContext就是典型的单例实现。

问:ThreadLocal 是单例模式吗?它的设计思想是什么?

  • 不是传统单例,而是 "线程封闭" 模式:为每个线程创建独立实例。

  • 原理:通过ThreadLocalMap将实例与线程绑定,以空间换时间实现线程安全。

📚 总结:单例模式的正确打开方式

  1. 优先选择枚举式:代码简洁,自动解决线程安全、反射和序列化问题。

  2. 复杂场景用 DCL:如需要延迟加载且对性能敏感时。

  3. 面试核心考点

  • 各种实现的线程安全差异。

  • 反射 / 序列化破坏单例的原理与解决方案。

  • 框架中的实际应用(如 Spring、JDK 的Runtime类)。

掌握这些要点,不仅能在开发中避免单例模式的坑,更能在面试中向面试官展示扎实的设计模式功底! 🚀