单例模式

57 阅读6分钟

单例模式分为懒汉式和饿汉式

饿汉式单例

类加载时就立即创建单例对象

public class HungrySingleton {

    // 1. 类加载时就创建对象
    private static final HungrySingleton instance = new HungrySingleton();

    // 2. 构造器私有化,防止外部创建
    private HungrySingleton() {
    }

    // 3. 对外提供获取实例的静态方法
    public static HungrySingleton getInstance() {
        return instance;
    }
}

运行机制

当 JVM 加载 HungrySingleton 类时:

  1. 静态变量 instance 被初始化;
  2. 立即执行 new HungrySingleton()
  3. 所以类加载完毕后,instance 已经是一个现成的单例对象。

无论调用多少次 getInstance() ,都返回同一个对象。

举例

public class DatabaseConnectionPool {
    private static final DatabaseConnectionPool pool = new DatabaseConnectionPool();

    private DatabaseConnectionPool() {
        System.out.println("连接池创建完成");
    }

    public static DatabaseConnectionPool getInstance() {
        return pool;
    }
}

当程序启动时,连接池就被提前创建,后续各个模块直接用,不需要再判断或加锁。
非常适合像“日志器”、“配置加载器”、“线程池”等对象

懒汉式单例

public class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (instance == null) {  // 第一次调用才创建
            instance = new LazySingleton();
        }
        return instance;
    }
}

饿汉-懒汉 对比

特性懒汉式饿汉式
创建时机第一次使用时类加载时
是否线程安全❌(需加锁)✅ JVM保证
性能⚠️ 稍低(加锁/DCL)✅ 高(无锁)
延迟加载✅ 是❌ 否
代码复杂度⚠️ 稍复杂✅ 简单
推荐写法✅ DCL 或静态内部类✅ 普通静态初始化

为什么懒汉式下线程不安全?

如果是单线程模式下,懒汉式没有线程安全问题。

但是如果是多线程模式下,就会出现问题。

初始化对象不是原子操作

instance = new LazySingleton();它有三个步骤;

  1. 分配内存空间
  2. 调用构造方法,初始化对象
  3. 将引用指向内存地址(instance指向这个对象)

编译器/CPU 可能发生 指令重排,顺序变为:

1️⃣ 分配内存
3️⃣ 指向地址
2️⃣ 初始化对象

在多线程模式下, 会导致另一个线程出错 :

假设线程 A 正在执行上面 3 步:

  • A 执行到第 3 步(引用已经指向地址,但对象还没初始化完)
  • 线程 B 此时执行 if (instance == null) → 判断为 false,直接返回这个“半初始化对象”
    → 这会引发 空指针对象状态异常

如何解决?

1. 加 synchronized 锁

整个方法加锁,性能开销大 ; 每次获取实例都要加锁,即使实例已经创建完毕。

public class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {}

    // 整个方法加锁,线程安全
    public synchronized static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

2.加上volatile

volatile 会禁止指令重排
确保初始化的顺序 严格按 1 → 2 → 3 执行。
同时也保证了 instance可见性(线程 B 能立刻看到线程 A 初始化完的结果)。

「懒汉式单例 + 双重检查锁定 DCL」
public class Singleton {
    private static volatile Singleton instance; //  volatile 关键

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查
                    instance = new Singleton(); //  非原子操作
                }
            }
        }
        return instance;
    }
}

synchronized (Singleton.class) 对这个类加锁;

为什么需要二次检查instance == null?第一次是判断是否已经被创建了,第二次是防止在第一次检查完是否创建,在加锁的前瞬间给其他线程船创建了。

与单纯的加锁synchronized的锁粒度不同,只有在第一次创建对象的时候加锁。

3.静态内部类单例

静态内部类单例不需要使用 volatile ,也天然不会被指令重排破坏。

原因:
静态内部类的实例化过程是由 JVM 类加载机制 保证线程安全的,
JVM 在类加载和初始化阶段就会自动加锁并禁止指令重排

public class Singleton {
    private Singleton() {}

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}
二者比较
特性双重检查锁(DCL)静态内部类
是否懒加载✅ 是✅ 是
是否需 volatile✅ 必须❌ 不需要
线程安全性依赖 volatile 保证JVM 类加载机制天然保证
是否可能被指令重排破坏是(未加 volatile)
性能较好(但有锁判断)最优(无锁)

反射可以破环单例

哪怕加了 volatile,也只能防止“线程安全问题”,但它防不了“暴力反射”。

例如:

Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s1 = constructor.newInstance();
Singleton s2 = constructor.newInstance();
System.out.println(s1 == s2); // ❌ false

因为反射可以直接调用私有构造函数,绕过你的单例逻辑。

静态内部类单例也可能被破坏

反射仍可通过 newInstance() 调用私有构造函数创建多个实例。

解决办法之一:

public class Singleton {
    private static volatile Singleton instance;
    // 用于标记是否已被实例化
    private static boolean initialized = false;

    private Singleton() {
        synchronized (Singleton.class) {
            if (initialized) {
                throw new RuntimeException("不要试图通过反射破坏单例!");
            }
            initialized = true;
        }
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

原理分析:

  • 第一次调用 getInstance() 时,构造函数执行 initialized = true
  • 若再用反射调用构造函数:
    • 会发现 initialized == true
    • 构造函数抛异常,拒绝创建第二个实例。

但是还是可以被破坏

反射修改 initialized 标志位; 那么依旧可以修改对应的信息导致破坏。

使用 Unsafe.allocateInstance()(绕过构造函数直接分配内存

枚举单例 —— 唯一不会被破坏的单例模式

public enum Singleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("Doing something...");
    }
}

什么是枚举(Enum)

枚举是一种特殊的类:

  • 每个枚举常量都是枚举类的 唯一实例
  • 枚举的构造器默认是 private
  • JVM 确保它只会被实例化一次
  • 自动实现 SerializableComparable 接口

为什么枚举单例不会被破坏

1. 防反射

JDK 源码中对 Enum 的反射构造有特殊保护:

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
    throw new IllegalArgumentException("Cannot reflectively create enum objects");

所以反射创建枚举实例会抛异常。

2.防反序列化

  • 普通单例可被反序列化破坏(通过 readObject 创建新对象);
  • 但枚举类型在反序列化时,JVM 内部使用 Enum.valueOf(),保证同一个实例。

3.线程安全 & 懒加载

  • 枚举类在第一次使用时才被加载(懒加载);
  • JVM 保证类加载过程的线程安全性。

总结

单例模式类型懒加载线程安全防反射防反序列化是否推荐
饿汉式一般
懒汉式 (同步方法)一般
DCL + volatile常用
静态内部类推荐
枚举单例✅✅✅ 推荐首选

这个串联其:volatile → 单例模式 → 反射破坏 → 枚举防御 JMM与Volatitle它并非真实存在的物理内存划分,而是Java虚拟机(JVM)定义的一套规则,用来屏蔽各种硬用来屏 - 掘金