单例模式:饿汉式和懒汉式的减肥故事

116 阅读2分钟

5.1 单例模式:饿汉式和懒汉式的减肥故事

graph TD
    A[单例模式] --> B[饿汉式]
    A --> C[懒汉式]
    B --> D[类加载就初始化]
    C --> E[延迟初始化]
    E --> F[线程安全版]
    E --> G[线程不安全版]
    F --> H[双重检查锁]
    F --> I[静态内部类]
    F --> J[枚举实现]

饿汉式:提前囤粮的焦虑症患者

// 像囤积症患者,一上来就把食物塞满冰箱
public class EagerSingleton {
    // 类加载时就初始化(JVM保证线程安全)
    private static final EagerSingleton instance = new EagerSingleton();
    
    // 把构造函数锁死,防止外部new实例
    private EagerSingleton() {
        if (instance != null) {
            throw new RuntimeException("单例禁止反射攻击!");
        }
    }
    
    public static EagerSingleton getInstance() {
        return instance;
    }
    
    // 测试代码
    public static void main(String[] args) {
        EagerSingleton s1 = EagerSingleton.getInstance();
        EagerSingleton s2 = EagerSingleton.getInstance();
        System.out.println(s1 == s2); // 输出 true
    }
}

特点:

  • ✅ 线程安全(类加载机制保证)
  • ❌ 可能浪费内存(即使不用也提前创建)
  • ❌ 无法防止反射攻击(示例中已添加防御代码)

懒汉式:临时抱佛脚的减肥达人

// 像节食者,只有饿到不行才去找吃的
public class LazySingleton {
    // volatile防止指令重排序(DCL必备)
    private static volatile LazySingleton instance;
    
    private LazySingleton() {}
    
    // 双重检查锁(Double-Check Locking)
    public static LazySingleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (LazySingleton.class) {
                if (instance == null) { // 第二次检查
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
    
    // 测试多线程环境
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        IntStream.range(0, 10).forEach(i -> 
            pool.submit(() -> 
                System.out.println(LazySingleton.getInstance().hashCode())
            )
        );
        pool.shutdown();
    }
}

执行结果: 所有线程输出的hashcode相同

特点:

  • ✅ 延迟加载节省资源
  • ✅ 线程安全(DCL+volatile)
  • ❌ 实现较复杂

面试题加油站 ⛽

  1. 为什么单例模式要私有构造方法?

    防止外部通过new创建实例,确保全局唯一性

  2. DCL为什么要双重检查?

    外层检查提高性能,内层检查防止多线程并发创建

  3. volatile关键字在DCL中的作用?

    禁止指令重排序,防止返回未初始化完成的对象

  4. 如何防止反射破坏单例?

    在构造方法中判断实例是否已存在,若存在则抛出异常

  5. 枚举实现单例的优势?

    天然防反射/反序列化攻击,代码简洁,推荐写法

  6. 单例模式违背了哪个设计原则?

    单一职责原则(既管理实例创建又承担业务逻辑)

  7. Spring框架中的单例是线程安全的吗?

    默认单例是非线程安全的,需要开发者自行保证状态安全


扩展小剧场(图解)

sequenceDiagram
    participant ThreadA
    participant ThreadB
    participant Singleton
    
    ThreadA->>Singleton: getInstance()
    Singleton-->>ThreadA: 第一次检查null
    ThreadA->>Singleton: 获取锁
    ThreadA->>Singleton: 第二次检查null
    Singleton-->>ThreadA: 创建实例
    ThreadA->>Singleton: 释放锁
    
    ThreadB->>Singleton: getInstance()
    Singleton-->>ThreadB: 第一次检查非null
    Singleton-->>ThreadB: 直接返回实例