设计模式-单例模式

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

实现方式 是否懒加载 是否安全 性能 推荐程度
饿汉式 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
懒汉式 ⭐⭐⭐ ❌ 不推荐
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、静态内部类等。

相关推荐
阿巴~阿巴~11 小时前
线程安全单例模式与懒汉线程池的实现与优化
linux·服务器·单例模式·线程池·饿汉模式·懒汉模式·静态方法
不光头强11 小时前
Java中的单例模式
单例模式
崎岖Qiu11 小时前
【设计模式笔记17】:单例模式1-模式分析
java·笔记·单例模式·设计模式
虾说羊14 小时前
创建对象中的单例模式
单例模式
安冬的码畜日常16 小时前
【JUnit实战3_27】第十六章:用 JUnit 测试 Spring 应用:通过实战案例深入理解 IoC 原理
spring·观察者模式·设计模式·单元测试·ioc·依赖注入·junit5
她说彩礼65万18 小时前
C#设计模式 单例模式实现方式
单例模式·设计模式·c#
安冬的码畜日常1 天前
【JUnit实战3_28】第十七章:用 JUnit 5 实测 SpringBoot 项目
spring boot·功能测试·测试工具·设计模式·单元测试·junit5
围巾哥萧尘1 天前
TRAE Agent 歌曲创作助手构建与使用教程🧣
设计模式
superman超哥1 天前
仓颉语言中流式I/O的设计模式深度剖析
开发语言·后端·设计模式·仓颉