单例模式(Singleton Pattern)

一句话

单例模式保证一个类在整个 JVM 生命周期中只有一个实例,并提供一个全局访问点。是 GoF 23 种设计模式中最简单、也最常被考察的模式之一。

适用场景

场景原因典型例子
无状态工具类无成员变量,不会因复用而出错RuntimeObjectMapper
共享资源避免重复创建、控制并发访问线程池、数据库连接池
配置类全局只有一份配置即可Spring @Configuration 的 Bean
日志/监控统一的日志输出或监控入口日志框架的 LoggerFactory

面试提示:记 2~3 个 JDK/框架中的单例就能说服面试官你真的用过。


一、饿汉式(Eager Initialization)

public class EagerSingleton {
    // 类加载时即创建实例(static final 保证线程安全)
    private static final EagerSingleton INSTANCE = new EagerSingleton();
    
    private EagerSingleton() {}  // 私有构造
    
    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

特点

维度评价
线程安全✅ 由类加载机制保证(JVM 保证 clinit 方法同步)
延迟加载❌ 类加载即创建,即使从未使用
实现复杂度⭐ 最简单
反序列化破坏❌ 需实现 readResolve()
反射破坏❌ 可通过 setAccessible(true) 调用私有构造

JDK 经典案例

Runtime.getRuntime() 就是标准的饿汉式单例:

// java.lang.Runtime(简化)
public class Runtime {
    private static final Runtime currentRuntime = new Runtime();
    public static Runtime getRuntime() { return currentRuntime; }
    private Runtime() {}
}

详见 Runtime类详解


二、懒汉式(Lazy Initialization)

基础版(非线程安全 ❌)

public class LazySingleton {
    private static LazySingleton instance;
    
    private LazySingleton() {}
    
    public static LazySingleton getInstance() {
        if (instance == null) {         // 多线程下可能同时进入
            instance = new LazySingleton();
        }
        return instance;
    }
}

非线程安全

两个线程同时进入 if (instance == null),会创建两个实例,违反单例。生产环境不要使用。

线程安全版(方法加锁 ✅)

public class LazySyncSingleton {
    private static LazySyncSingleton instance;
    
    private LazySyncSingleton() {}
    
    public static synchronized LazySyncSingleton getInstance() {
        if (instance == null) {
            instance = new LazySyncSingleton();
        }
        return instance;
    }
}
维度评价
线程安全synchronized 保证互斥
性能❌ 每次获取实例都要加锁,高并发下性能瓶颈明显

问题在于:实例创建后,所有读操作都被 synchronized 串行化了,而我们只需要在第一次创建时同步。


三、双重检查锁定(Double-Checked Locking, DCL) ⭐

public class DCLSingleton {
    // volatile 禁止指令重排序,防止读到未初始化完成的对象
    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;
    }
}

为什么需要 volatile

instance = new DCLSingleton() 在字节码层面分三步:

1. memory = allocate();    // 分配内存空间
2. initInstance(memory);   // 初始化对象
3. instance = memory;      // 将引用指向内存地址

如果 没有 volatile,步骤 2 和 3 可能被重排序为 1→3→2。此时另一个线程在第一重检查发现 instance != null,直接返回一个未初始化完成的对象,访问其字段会崩溃。

volatile 通过内存屏障禁止步骤 2/3 重排序,详见 典型应用场景

特点

维度评价
线程安全✅ volatile + synchronized
延迟加载✅ 第一次调用时才创建
性能✅ 创建后无锁访问(只有第一次进入同步块)
实现复杂度⭐⭐⭐ 需理解 volatile 和指令重排序
反序列化破坏❌ 需实现 readResolve()
反射破坏❌ 可通过反射调用私有构造

面试最常考察的实现方式,重点理解 volatile 的作用。


四、静态内部类(Initialization-on-Demand Holder) ⭐

public class HolderSingleton {
    
    private HolderSingleton() {}
    
    // 静态内部类在 HolderSingleton 加载时不会初始化
    private static class Holder {
        private static final HolderSingleton INSTANCE = new HolderSingleton();
    }
    
    public static HolderSingleton getInstance() {
        return Holder.INSTANCE;  // 触发 Holder 类加载,创建实例
    }
}

原理

  • JVM 加载 HolderSingleton 时,不会加载内部类 Holder
  • 第一次调用 getInstance() 时,JVM 加载 Holder 类,触发其 clinit 方法创建实例
  • JVM 保证类的 clinit 是线程安全的(同一类在同一时间只有一个线程能执行 clinit

特点

维度评价
线程安全✅ JVM 类加载机制保证
延迟加载✅ 内部类在 getInstance() 首次访问时加载
性能✅ 无锁,类加载后开销为零
实现复杂度⭐⭐ 简洁且无额外关键字
反序列化破坏❌ 需实现 readResolve()

推荐的 Java 单例实现方式 —— 兼具延迟加载、线程安全、零同步开销。比 DCL 更简洁、更安全。


五、枚举实现(Enum Singleton) ⭐⭐⭐

public enum EnumSingleton {
    INSTANCE;
    
    private String config;
    
    public void doSomething() {
        System.out.println("执行单例方法");
    }
    
    // 可以有成员变量和方法
    public String getConfig() { return config; }
    public void setConfig(String config) { this.config = config; }
}

使用方式:

EnumSingleton.INSTANCE.doSomething();
EnumSingleton.INSTANCE.setConfig("prod");

特点

维度评价
线程安全✅ 枚举实例创建由 JVM 保证
延迟加载❌ 枚举类加载时即创建(近似饿汉式)
反序列化破坏天然防御 — JVM 保证每个枚举常量只有一个实例
反射破坏天然防御 — 反射不能通过构造创建枚举
实现复杂度⭐ 最简单

Joshua Bloch(Effective Java 作者)强烈推荐枚举实现 —— 它是唯一能完全防御反射和序列化攻击的单例实现。

枚举 vs 其他实现的防御对比

// ❌ 普通单例:反射破坏
Constructor<?> constructor = DCLSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
DCLSingleton another = (DCLSingleton) constructor.newInstance();  // 成功创建第二个实例
 
// ✅ 枚举单例:反射无法破坏
Constructor<?> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
EnumSingleton another = (EnumSingleton) constructor.newInstance();
// 抛出 java.lang.IllegalArgumentException: Cannot reflectively create enum objects

来自《Effective Java》第 3 条:“使用枚举实现单例”


六、各实现方式对比总表

实现方式线程安全延迟加载防反射防序列化性能推荐度
饿汉式⭐⭐⭐⭐⭐
懒汉式(非同步)⭐⭐⭐❌勿用
懒汉式(同步)
DCL 双重检查⭐⭐⭐⭐⭐⭐
静态内部类⭐⭐⭐⭐⭐⭐⭐
枚举⭐⭐⭐⭐⭐⭐⭐⭐

七、破坏单例的几种方式

1. 反射攻击

// 防御方式:在私有构造中加标志位
public class SafeSingleton {
    private static boolean initialized = false;
    
    private SafeSingleton() {
        synchronized (SafeSingleton.class) {
            if (initialized) {
                throw new RuntimeException("单例被破坏!");
            }
            initialized = true;
        }
    }
}

2. 序列化破坏

// 防御方式:实现 readResolve() 方法
public class SerializableSingleton implements Serializable {
    private static final SerializableSingleton INSTANCE = new SerializableSingleton();
    
    private SerializableSingleton() {}
    
    // 反序列化时直接返回已有实例,而不是创建新对象
    private Object readResolve() {
        return INSTANCE;
    }
}

3. 克隆破坏

// 防御方式:重写 clone() 方法
@Override
public Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException("单例不允许克隆");
    // 或直接 return INSTANCE;
}

八、Spring 中的单例

Spring IoC 容器的默认 scope 是 singleton,但不同于 GoF 单例

对比维度GoF 单例Spring 单例
控制方式类自身控制(private 构造)容器控制(IoC 容器管理)
实例个数整个 JVM 唯一每个容器一个实例(多个容器可有多个)
实现方式编码实现默认 scope,无特殊限制
线程安全需自行保证状态安全无状态 Bean 安全,有状态需额外处理
@Component  // Spring 默认就是单例
public class UserService {
    // 无状态服务,天然线程安全
}

九、代码选择指南

graph TD
    Start["需要单例吗?"] --> Q1{"需要延迟加载?"}
    
    Q1 -->|"是"| Q2{"需要防御反射/序列化?"}
    Q1 -->|"否"| Q3{"需要防御反射/序列化?"}
    
    Q2 -->|"是"| Enum["🔸 枚举<br/>(推荐)"]
    Q2 -->|"否"| Holder["🔸 静态内部类<br/>(最简洁)"]
    
    Q3 -->|"是"| Enum2["🔸 枚举<br/>(推荐)"]
    Q3 -->|"否"| Eager["🔸 饿汉式<br/>(最简单)"]
    
    Enum --> NoteEnum["⚠️ 不能延迟加载<br/>但防御最完备"]
    Holder --> NoteHolder["✅ 推荐<br/>兼顾延迟加载与简洁"]
    Eager --> NoteEager["✅ 常用于工具类<br/>如 Runtime"]

相关笔记