单例模式有几种写法?


面试考察点

  1. 基础掌握度:面试官不仅仅是想知道你背了几种写法,更是想看你能否说出每种写法的优缺点和适用场景,而不是只会一种 "背答案"。

  2. 多线程安全意识:单例模式最核心的考点就是线程安全。面试官想看你是否理解并发环境下的问题,以及如何优雅地解决——这才是拉开差距的地方。

  3. JVM 原理深度:能否讲清楚 volatile 的作用、类加载机制、序列化破坏单例等底层原理,是区分 "会用" 和 "真正懂" 的关键。

核心答案

先说结论:单例模式主要有 5 种经典写法——

写法线程安全延迟加载推荐指数
饿汉式✅ 安全❌ 否⭐⭐⭐
懒汉式(同步方法)✅ 安全✅ 是
双重检查锁(DCL)✅ 安全✅ 是⭐⭐⭐⭐
静态内部类✅ 安全✅ 是⭐⭐⭐⭐⭐
枚举单例✅ 安全❌ 否⭐⭐⭐⭐⭐

一句话总结:一般场景推荐静态内部类,需要防反射和序列化破坏时用枚举单例。

深度解析

一、饿汉式

类加载时就创建实例,简单粗暴,天生线程安全。

public class Singleton {
    // 类加载时就初始化
    private static final Singleton INSTANCE = new Singleton();

    // 私有构造器,防止外部 new
    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

优点是写法简单,线程安全(JVM 保证类加载过程的线程安全性)。缺点也很明显——不管用不用都先创建,浪费资源。如果你的单例对象很大但未必会用到,这就不合适了。

二、懒汉式(同步方法)

用到的时候才创建,但为了线程安全给整个方法加了 synchronized

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    // 整个方法加锁,性能堪忧
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

说实话,这种写法在面试里就是用来 "踩" 的。虽然线程安全,但每次调用 getInstance() 都要获取锁,而实际上只有第一次创建时才需要同步。这在高并发场景下性能损耗非常大,生产环境基本不用。

三、双重检查锁(DCL)

这个是面试的 "重头戏",也是面试官最爱追问的写法。

public class Singleton {
    // 注意:volatile 很关键,不能省!
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        // 第一次检查,避免不必要的加锁
        if (instance == null) {
            synchronized (Singleton.class) {
                // 第二次检查,防止多个线程同时通过第一次检查
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这里有一个非常关键的细节——为什么必须加 volatile

这涉及到 JVM 的指令重排序问题。new Singleton() 这行代码在 JVM 层面并不是一个原子操作,它分三步:

上面的图展示了指令重排序导致的问题。具体来说:

  • 正常顺序:① 分配内存 → ② 初始化对象 → ③ 指向引用。一切正常。
  • 重排序后:① 分配内存 → ③ 指向引用 → ② 初始化对象。此时如果线程 A 执行到 ③,还没来得及执行 ②,线程 B 判断 instance != null,直接返回了一个半成品对象,使用时就会 NPE 或数据错乱。

加上 volatile 就是利用它的禁止指令重排序语义(内存屏障),保证 ①②③ 严格按照顺序执行。JDK 5 之后 volatile 才真正靠谱,所以 DCL 要求 JDK 5+。

四、静态内部类(推荐)

这是我个人最推荐的写法,优雅且无锁。

public class Singleton {

    private Singleton() {}

    // 静态内部类,外部类加载时不会立即加载
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

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

为什么说它好?利用了 JVM 的类加载机制来保证线程安全——外部类加载时并不会立即加载静态内部类,只有在调用 getInstance() 触发 Holder.INSTANCE 时,Holder 类才会被加载和初始化。而 JVM 在类加载初始化阶段会加锁,天然保证了线程安全,还实现了延迟加载。

无锁、线程安全、延迟加载、写法简洁,几乎没有缺点。唯一的问题是可以通过反射破坏单例。

五、枚举单例

Effective Java 作者 Josh Bloch 最推崇的写法。

public enum Singleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("单例方法");
    }
}

// 使用
Singleton.INSTANCE.doSomething();

枚举单例号称 "最完美的单例",原因有三:

  • 线程安全:枚举类的实例创建由 JVM 在类加载时保证
  • 防反射:反射尝试通过 newInstance() 创建枚举对象时,JVM 会直接抛异常
  • 防序列化:枚举的序列化机制由 JVM 保证不会创建新对象

别的写法通过反射或序列化/反序列化都能破坏单例,只有枚举天然免疫。

深度补充:单例的 "破坏" 与 "防御"

面试官有时会追问:单例能被破坏吗? 答案是可以的,主要有两种攻击方式:

1. 反射破坏

Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true); // 破坏 private 限制
Singleton broken = constructor.newInstance(); // 创建了第二个实例!

防御方式:在私有构造器中加判断——

private Singleton() {
    if (Holder.INSTANCE != null) {
        throw new RuntimeException("不允许反射创建实例");
    }
}

2. 序列化/反序列化破坏

// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.obj"));
oos.writeObject(instance);
oos.close();

// 反序列化 —— 产生新对象!
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.obj"));
Singleton broken = (Singleton) ois.readObject();

防御方式:实现 readResolve() 方法——

private Object readResolve() {
    return getInstance(); // 返回已有实例,而不是创建新的
}

当然,用枚举单例就完全不需要操心这些。

面试高频追问

  1. 为什么 DCL 必须加 volatile?不加会怎样?

    • 防止指令重排序导致获取到未初始化的对象。不加的话在高并发下可能出现 NPE。
  2. 单例模式能被反射破坏吗?怎么防?

    • 能。构造器里加判断,或者直接用枚举。
  3. 单例在分布式环境下还有效吗?

    • 无效。单例只保证 JVM 进程内唯一,分布式环境下每个 JVM 都有自己的单例。分布式场景需要用分布式锁或集中式存储来保证。
  4. Spring 的 Bean 默认是单例吗?和设计模式的单例一样吗?

    • Spring Bean 默认 scope=singleton,但它是由 Spring 容器管理的单例,和传统单例模式不同。Spring 单例是每个容器内唯一,而且 Spring 处理了线程安全问题。

常见面试变体

  • "手写一个线程安全的单例"
  • "单例模式怎么防止反射和序列化攻击?"
  • "你项目中哪个地方用到了单例模式?"
  • "Spring 的单例和设计模式的单例有什么区别?"

记忆口诀

五种写法:饿(饿汉)懒(懒汉)双(DCL)内(静态内部类)枚(枚举)——"饿懒双内枚"

DCL 三要素:两次检查一把锁,volatile 不能少。

推荐选择:一般用静态内部类,防攻击用枚举。

总结

面试答单例,先说五种写法的名称和特点,再重点展开 DCL 的 volatile 原理(指令重排序),最后提一嘴反射和序列化破坏以及枚举的完美防御。这条线拉下来,面试官基本满意。