单例模式 懒汉式(静态内部类)

/**
 * 单例模式
 * 懒汉式 方式6 静态内部类
 * 优点:利用JVM类加载机制保证延迟加载和线程安全。
 */
public class SingletonInnerClass {
    // 1、私有构造方法
    private SingletonInnerClass() {}

    private static class SingletonInnerClassHolder {
        // 2、声明对象
        private static final SingletonInnerClass INSTANCE = new SingletonInnerClass();
    }

    // 3、对外提供public方法获取唯一实例
    public static SingletonInnerClass getInstance() {
        return SingletonInnerClassHolder.INSTANCE;
    }

}

这种写法巧妙地利用了 Java 语言的特性,把“多线程同步”和“内存可见性”等复杂问题,全部甩锅给了 JVM 处理。

1. 为什么它是“懒加载”的?(Lazy Loading)

根本原因:加载外部类,不会自动加载静态内部类。

  • 启动时: 当程序启动,或者用到 SingletonInnerClass 这个外部类时,JVM 加载了外部类。但此时,内部类 SingletonInnerClassHolder 并没有被加载
  • 运行时: 只有当第一次调用 getInstance() 方法时,代码里引用了 SingletonInnerClassHolder.INSTANCE
  • 触发点: 此时,JVM 才会去加载、连接、初始化这个内部类。
  • 结果: 这就实现了“不到万不得已(调用 getInstance),绝不创建对象”的懒加载效果。

2. 为什么它是“线程安全”的?

没有在代码里写任何 synchronized,那并发时怎么办?

根本原因:JVM 在加载类的时候,是自带“锁”的。

  • JVM 的承诺: 虚拟机会保证一个类的初始化(Initialization)过程在多线程环境中被正确地加锁、同步。
  • 过程还原:
  1. 线程 A 和线程 B 同时调用 getInstance()
  2. 它们同时试图访问 SingletonInnerClassHolder
  3. JVM 发现这个内部类还没加载,于是开始加载它。
  4. JVM 自动加锁: JVM 保证只有一个线程能执行这个类的初始化代码(即执行 static 块或初始化 static 变量)。
  5. 线程 A 抢到了初始化权,执行 new SingletonInnerClass()
  6. 线程 B 只能等待。
  7. 等 A 初始化完毕,锁释放。B 拿到的就是已经完全创建好的 INSTANCE

结论: 这里的线程安全不是靠你写的代码保证的,而是靠 JVM 底层的类加载锁 保证的。


3. 为什么不需要 volatile

在双重检查锁(DCL)中,我们需要 volatile 是为了防止指令重排序导致其他线程拿到“半成品”对象。

在这里不需要 volatile,是因为 Java 内存模型 (JMM) 对类的初始化阶段做了特殊的规定(Happens-Before 关系):

  • 类加载的屏障: 只有当一个类被完全初始化之后,它的静态变量才会被暴露给其他线程。
  • 原子性保证: static final ... INSTANCE = new ... 这行代码是在类加载阶段执行的。JVM 保证了在类加载完成之前,其他线程根本看不到 INSTANCE 这个变量。
  • 结果: 当其他线程能访问到 INSTANCE 时,它一定已经是一个初始化完成的完整对象了。不存在“先赋值引用、后初始化构造方法”的重排序并发风险。

posted @ 2026-01-28 22:02  Nickey103  阅读(34)  评论(0)    收藏  举报