静态内部类单例线程安全,因JVM保证<clinit>只执行一次且加锁同步;必须用static修饰,否则隐式持外部类引用、破坏懒加载与单例语义。

静态内部类必须用 static 修饰,否则无法保证线程安全
普通内部类隐式持有外部类引用,且依赖外部类实例才能加载;而单例要的是“不依赖实例、只加载一次”的行为。只有 static 修饰的内部类才满足:它属于类级别,不绑定任何对象,JVM 在首次主动使用时才触发其初始化(即执行 <clinit></clinit>)。
常见错误是漏写 static,写成:private class SingletonHolder —— 这会导致编译通过但运行时行为失控:每次 new 外部类都可能触发内部类加载,彻底破坏单例语义。
- 必须声明为
private static class SingletonHolder - 内部类里不能有非静态成员(包括非静态方法或字段),否则编译报错
- 外部类构造函数必须是
private,否则可通过反射或直接 new 绕过控制
getInstance() 方法里不能提前引用 SingletonHolder 类名
看似无害的写法,比如在外部类静态字段中写 private static final Class> HOLDER = SingletonHolder.class;,会强制 JVM 在外部类加载阶段就初始化 SingletonHolder,从而失去懒加载特性——实例在应用启动时就被创建,和饿汉式无异。
真正安全的触发方式只有一种:在 getInstance() 方法体内,通过读取 SingletonHolder.INSTANCE 这个静态字段来间接引用该类。
- ✅ 正确:
return SingletonHolder.INSTANCE; - ❌ 错误:
Class.forName("Singleton$SingletonHolder");或SingletonHolder.class出现在静态初始化块/字段中 - ⚠️ 注意:IDE 可能自动补全
SingletonHolder类名并加 import,但只要没在字节码层面触发首次主动使用,就不算违规
反序列化会破坏单例,readResolve() 不可省略
即使类加载机制保证了构造过程的线程安全,ObjectInputStream 仍可能绕过构造器直接还原对象,导致多个实例存在。这是所有基于私有构造的单例共有的漏洞,静态内部类也不例外。
修复方式是在外部类中显式定义 readResolve() 方法,返回已有实例:
private Object readResolve() {
return getInstance();
}
- 该方法必须是
private,返回类型为Object,签名严格匹配 - 如果用了 Lombok 的
@AllArgsConstructor或类似注解,需确认未自动生成无参构造或覆盖该方法 - 测试时可用
SerializationUtils.clone(instance)(Apache Commons Lang)验证是否仍是同一对象
为什么不用 synchronized 却天然线程安全
关键不在代码逻辑,而在 JVM 规范对类初始化的约束:<clinit></clinit> 方法由类加载器加锁执行,且每个类有且仅有一个 <clinit></clinit> 执行机会。当多线程同时首次访问 SingletonHolder.INSTANCE,只有一个线程能进入初始化流程,其余阻塞等待,完成后全部拿到同一个实例。
这和双重检查锁不同——后者靠应用层同步,前者靠 JVM 底层机制,无额外性能损耗,也不依赖 volatile。
- 不需要
volatile,因为实例创建发生在类初始化阶段,JMM 保证该过程对所有线程可见 - 不能手动调用
SingletonHolder.class.getClassLoader().loadClass(...)来“预热”,否则同样提前触发初始化 - 类加载锁是 JVM 内部实现细节,开发者不可见、不可干预,但可完全信赖
getInstance() 里写了 System.out.println(SingletonHolder.class.getName());,只要没读取 INSTANCE 字段,就不会初始化。这个边界很细,但决定了懒加载是否生效。

















