设计模式-单例模式

🔷懒加载,安全,性能,推荐程度对比

实现方式 是否懒加载 是否安全 性能 推荐程度
饿汉式 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
懒汉式 ⭐⭐⭐ ❌ 不推荐
synchronized 懒汉式 ⭐⭐
DCL 双重检查 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
静态内部类 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐(最推荐)
枚举 ✅✅✅ ⭐⭐⭐⭐⭐ ✅ 强安全推荐

🔷饿汉式

类加载的时候立即创建实例,线程安全,简单,但是美中不足的是没有办法懒加载

java 复制代码
package com.zr.singleton;

/**
 * @Author: zr
 * @Date: 2025/11/04/17:11
 * @Description:
 */

/**
 * 饿汉式单例
 * 特点: 类加载的时候立即创建实例,线程安全,简单,但是美中不足的是没有办法懒加载
 */
public class Singleton1 {
    // JVM在类加载的时候就创建单例对象
    private static final Singleton1 INSTANCE = new Singleton1();
    // 获取单例对象
    private static Singleton1 getInstance() {
        return INSTANCE;
    }
}

🔷懒汉式

第一次使用时才会创建,但是在多线程场景下会出现线程不安全的问题,但是可以懒加载

java 复制代码
package com.zr.singleton;

/**
 * @Author: zr
 * @Date: 2025/11/04/17:16
 * @Description:
 */

/**
 * 懒汉式单例
 * 特点: 第一次使用时才会创建
 * 但是在多线程场景下会出现线程不安全的问题,
 * 但是可以懒加载
 */
public class Singleton2 {
    // 先不创建对象,使用的时候才创建
    private static Singleton2 instance;
    private Singleton2() {}

    //线程不安全:多个线程可能同时进入此方法,导致创建多个实例
    public static Singleton2 getInstance(){
        // 第一次进入时instance为null
        if (instance == null){
            // 可能多个线程执行到这,导致创建多个实例
            instance = new Singleton2();
        }
        return instance;
    }
}

🔷懒汉式+synchronized

线程安全,但每次调用都加锁,性能差

java 复制代码
package com.zr.singleton;

/**
 * @Author: zr
 * @Date: 2025/11/04/17:22
 * @Description:
 */

/**
 * 懒汉式单例+synchronized
 * 特点: 线程安全,但每次调用都加锁,性能差
 */
public class Singleton3 {
    private static Singleton3 instance;
    private Singleton3() {}
    // 此处加锁保证每次只有一个线程进入方法,但是这样会导致在高并发情况下性能差
    public static synchronized Singleton3 getInstance(){
        if (instance == null){
            instance = new Singleton3();
        }
        return instance;
    }
}

🔥DCL (Double-Check Locking)双重检查

线程安全,懒加载,性能高,实际项目中的常用方案

java 复制代码
package com.zr.singleton;

/**
 * @Author: zr
 * @Date: 2025/11/04/17:25
 * @Description:
 */

/**
 * 双重检查锁DCL(Double-Check Locking)
 * 特点: 线程安全,懒加载,性能高,实际项目中的常用方案
 */
public class Singleton4 {
    // 必须volatile,防止指令重排序,导致线程获取未初始化的对象
    private static volatile Singleton4 instance;
    private Singleton4(){}
    public static Singleton4 getInstance(){
        // 第一重检查(无锁提升性能),第一重检查会拦截掉大部分线程,提高性能,只要instance实例化后,
        // 之后所有线程都不会进入第二重检查(加锁),导致相比于懒汉式单例+synchronized性能会高很多
        if (instance==null){
            // 第二重检查(有锁) ,少部分线程,在第一重检查突的情况下,在此处加锁防止创建多个对象
            synchronized (Singleton4.class){
                if (instance==null){
                    instance = new Singleton4();
                }
            }
        }
        return instance;
    }
}

✅ 1. 为什么 volatile 能防止指令重排?

因为 volatile 在 JVM 层加入了内存屏障(Memory Barrier),禁止了以下两类重排序:

  • 写屏障(Store Barrier):禁止构造函数初始化之前,对变量的写入被重排序。
  • 读屏障(Load Barrier):禁止读取到尚未初始化完成的对象引用。

也就是说:

使用 volatile 修饰对象引用,可以避免 "new 对象" 的重排序。


✅ 2. 如果没有 volatile,会发生什么指令重排?

我们看 DCL 单例中关键的这句:

java 复制代码
instance = new Singleton();

这句语句在 JVM 底层不是原子操作,会被拆成 3 步:

✅ 正常的执行顺序(没有重排)

  1. 分配内存
  2. 调用构造函数进行初始化
  3. 将对象引用赋给 instance

可能发生的重排序顺序(危险!)

JVM 允许步骤 2 和 3 交换顺序

  1. 分配内存
  2. 先把内存地址赋值给 instance(但对象还没初始化完成!)
  3. 再调用构造函数初始化

✅ 3. 多线程下导致的实际 BUG(最危险的点)

示例场景:

  • 线程 A 正在执行 new Singleton(),并且发生了重排序
  • 线程 B 同时进入 getInstance() 并看到 instance != null

✅ 此时出现问题:

线程 B 拿到的是一个 尚未初始化完成的对象

(构造函数还没执行完)

这会导致:

  • 成员变量仍是默认值(null、0、false)
  • 对象处于不一致/脏状态
  • 随机 NPE、业务逻辑错误
  • 极低概率但极难排查的并发 bug

这就是为什么双重检查锁(DCL)如果没有 volatile 会被认为是错误的实现。


✅ 4. volatile 如何解决?

加上 volatile 后:

plain 复制代码
private static volatile Singleton instance;

JVM 在写入 instance 时会插入:

写屏障 :初始化完成前禁止把引用暴露出去

读屏障:禁止线程读取到一个半初始化的对象

➡️ 禁止了重排序

➡️ 也保证多线程可见性


✅ 5. 图示理解(最直观)

❌ 失败的情况(无 volatile):

plain 复制代码
线程A:instance = new Singleton():
    ├── 分配内存
    ├── instance 指向这块内存(B 可见)
    └── 执行构造
线程B:if (instance != null) { 使用它 }  // 🚨 使用未初始化对象

✅ 正确情况(加 volatile):

plain 复制代码
线程A:
    分配内存
    执行构造
    写屏障
    instance 指向这块内存(B 可见)

线程B:
    读屏障保证读到的是初始化后的对象

✅ 6. 为什么静态内部类不需要 volatile?

因为 静态内部类Holder 模式依赖 JVM 类初始化,而类初始化过程:

✅ 本身是线程安全的

✅ 不会进行重排序

✅ 只执行一次

所以 静态内部类实现不需要 volatile

🔥静态内部类

  • 线程安全: 由JVM保证类加载的原子性
  • 懒加载: 静态内部类Holder类,只会在首次访问时才会加载
  • 性能高: 无锁
java 复制代码
package com.zr.singleton;

/**
 * @Author: zr
 * @Date: 2025/11/04/17:38
 * @Description:
 */

/**
 * 静态内部类单例
 * 特点: 线程安全: 由JVM保证类加载的原子性
 * 懒加载: Holder类,只会在首次访问时才会加载
 * 性能高: 无锁
 */
public class Singleton5 {
    private Singleton5(){}
    //静态内部类持有单例对象,只有调用getInstance()方法时才会触发Holder类加载
    private static class Holder {
        private static final Singleton5 INSTANCE = new Singleton5();
    }
    // 获取单例对象
    public static Singleton5 getInstance(){
        return Holder.INSTANCE;
    }
}

✅ 1. 为什么线程安全?

原因:依赖 JVM 对类加载(Class Loading) 和 初始化(Class Initialization)的严格线程安全保证。

✅ JVM 规定:类初始化是同步的(synchronized)

当某个类 第一次被主动使用 时,JVM 会对其执行初始化(initialization),具体步骤:

  • 只有 一个 线程能执行该类的 <clinit>(静态代码块 + 静态变量初始化)
  • 其他线程必须阻塞等待
  • 当初始化完成后,类被标记为 "已初始化"
  • 之后任何线程访问该类,不再重复初始化
✅ 应用到 Holder 模式:
plain 复制代码
public class Singleton {

    private Singleton() {}

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

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

当执行 Holder.INSTANCE 时:

➡️ JVM 会加载 Holder 类

➡️ 初始化时会执行一个 原子且线程安全的步骤 :创建 INSTANCE

➡️ 多线程环境下类只初始化一次,因此 INSTANCE 不会被创建多次


✅ 2. 为什么懒加载?

原因:静态内部类只有在第一次被使用时才会被加载。

✅ JVM 类加载触发条件(主动使用)包括:

  • new
  • 调用静态变量
  • 调用静态方法
  • 反射
  • 初始化子类

但是 不包含

  • 外部类被加载
  • 外部类被初始化
  • 外部类实例化

✅ 对于 Holder 模式:

  • Singleton 类加载时,Holder 类不会被加载
  • 只有当 getInstance() 被调用,真正访问 Holder.INSTANCE 时,
    JVM 才会加载 Holder 并创建 INSTANCE

➡️ 所以达成了真正意义的 Lazy Loading(延迟加载)


✅ 最终总结

特性 原因
线程安全 JVM 类初始化天然同步,保证静态变量只初始化一次
懒加载 静态内部类在首次使用时才会加载

✅ 用一句话形容静态内部类单例:

利用 JVM 类加载机制保证线程安全,用内部类延迟实例化,性能极高,不需要 synchronized。

🔥枚举

  • 天生线程安全
  • 防止反射攻击,防止反序列化破坏
  • 写法简单
java 复制代码
package com.zr.singleton;

/**
 * @Author: zr
 * @Date: 2025/11/04/17:43
 * @Description:
 */

/**
 * 枚举单例
 * 特点: 天生线程安全
 * 安全: 防止反射攻击,防止反序列化破坏,
 * 简单: 写法简单
 */
public enum Singleton6 {
    // 枚举单例
    INSTANCE;
    public void doSomething(){
        System.out.println("Using Enum Singleton");
    }

    // 使用方式:
//    public static void main(String[] args) {
//        // 直接使用 INSTANCE
//        Singleton6.INSTANCE.doSomething();
//    }
}

枚举(Enum)之所以 "天生线程安全" ,不是因为它有特殊的魔法,而是因为:
JVM 对枚举的创建方式 + 类加载机制 + 枚举语义三者结合,使其必然只能生成 1 个实例,并且线程安全。

下面我按"JVM 层面的原理"给你讲透。


✅ 原因一:枚举实例在类加载阶段由 JVM 保证只初始化一次(线程安全)

枚举类在编译后,本质上会被编译成普通类:

plain 复制代码
public final class Singleton extends Enum<Singleton> {
    public static final Singleton INSTANCE = new Singleton();
}

关键点在这里:

  • INSTANCEpublic static final
  • JVM 对 "类的初始化阶段" 有明确的线程安全保证

JVM 规范:类初始化()方法由 JVM 保证是同步的,同一时刻只能有一个线程执行。

因此:

✔ 类加载时,只会创建一次 INSTANCE

✔ 绝不会出现多个线程同时创建实例的情况

这就是天生线程安全的底层原因。


✅ 原因二:枚举构造函数只能被调用一次

普通类可以通过:

  • new
  • 反射
  • 克隆
  • 反序列化

创建多个实例。

但枚举类不行:

✅ 1. 构造器自动是 private,且只能被 JVM 调用

开发者无法 new。

✅ 2. 反射无法创建枚举对象

用反射构造会抛出异常:

plain 复制代码
Cannot reflectively create enum objects

✅ 3. 枚举反序列化不会创建新实例

普通单例会被反序列化破坏,需要 readResolve() 修补。

但枚举:

反序列化时不会重新创建对象,只会返回同一个枚举常量。

✔ 你无法破坏枚举的单例性

✔ JVM 硬性保证枚举常量永远是全局唯一的对象

所以它是最牢固的单例模式


✅ 原因三:枚举实例在 JVM 中以线程安全的方式初始化并存放在方法区

枚举常量会在类加载时初始化,并存放在方法区(或 metaspace)。

这个过程:

  • 由 JVM 控制
  • 原子性保证
  • 且类初始化一定是线程安全

因此:

✔ 不会在多线程下初始化两次

✔ 不存在竞态条件

✔ 实例天然唯一


✅ 原因四:没有延迟加载 → 避免并发初始化问题

枚举常量不是懒加载,而是:

在类加载阶段就初始化好

这避免了懒汉式、DCL 等单例中可能遇到的并发抢初始化问题。

因此:

✔ 枚举不会有"多线程抢着初始化"的问题

✔ 懒加载单例都会面临的线程安全风险在枚举中不存在


✅总结:为什么枚举天生线程安全?

原因 说明
✅ JVM 保证类初始化 (<clinit> ) 只会执行一次 这是最核心原因
✅ 枚举实例在加载阶段完成初始化 避免并发初始化
✅ 枚举构造函数只能被 JVM 调用一次 保证唯一
✅ 反射不能创建枚举对象 防止破坏单例
✅ 反序列化不会创建新对象 防止序列化攻击
✅ INSTANCE 是 public static final 和类常量一样安全且唯一

👉 这些保证叠加,使得枚举成为最安全、最稳固的单例实现方式,远强于 DCL、静态内部类等。

相关推荐
触底反弹35 分钟前
🤯 面试被问 AI Workflow 和 Agent 有啥区别?3 张图 + 2 段代码讲清楚!
人工智能·设计模式·面试
张小姐的猫18 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
杨充21 小时前
10.可测试性实战设计
设计模式·开源·代码规范
杨充21 小时前
9.重构十二式的实战
设计模式·开源·代码规范
杨充21 小时前
6.设计原则的全景图
设计模式·开源·全栈
杨充21 小时前
2.面向对象的特性
设计模式
杨充21 小时前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充21 小时前
8.反模式与坏味道
设计模式·开源·代码规范
杨充21 小时前
3.接口vs抽象类比较
设计模式