静态内部类单例通过JVM类初始化机制保证线程安全,无需显式同步;其核心是private static class SingletonHolder中定义final INSTANCE,并在getInstance()中首次引用触发懒加载。

为什么静态内部类能保证线程安全
因为 JVM 保证类的初始化是线程安全的,SingletonHolder 类第一次被主动引用时才触发初始化,而这个“第一次”由 JVM 锁住,天然串行。不像双重检查锁(DCL)需要自己写 synchronized 和 volatile,这里连锁都不用显式加。
常见错误现象:有人把 getInstance() 写成 public static Singleton getInstance() { return new Singleton(); } —— 这根本没用内部类,每次调用都新建实例;或者把 SingletonHolder 声明为 public,外部误触发加载,提前初始化。
-
SingletonHolder必须是 private static class,且仅在getInstance()中被引用 - 外部代码不能通过反射或类加载器提前触碰
SingletonHolder.class - 构造函数仍需设为 private,防止反射攻击(虽然静态内部类本身不防反射,但这是兜底)
静态内部类单例的标准写法
核心就三块:私有构造、静态内部类、对外暴露的 getInstance()。没有多余字段,不依赖任何第三方工具类,JDK 1.2+ 全支持。
典型误用:在 SingletonHolder 里加静态代码块或复杂初始化逻辑——这会拖慢首次调用速度,且一旦抛异常,后续调用全失败(JVM 会标记该类初始化失败,再访问直接抛 NoClassDefFoundError)。
立即学习“Java免费学习笔记(深入)”;
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
-
INSTANCE必须是final,否则可能被反射修改(虽概率低,但破坏单例语义) - 不要在
SingletonHolder中调用外部服务、读配置文件等耗时/可能失败的操作 - 如果真需要延迟初始化 + 复杂逻辑,应改用
java.util.concurrent.ConcurrentHashMap或AtomicReference配合 DCL
它和双重检查锁(DCL)比有什么实际差异
性能上几乎无差别(HotSpot 对静态初始化做了充分优化),但 DCL 的 volatile 字段写入在某些旧 JVM 或弱内存模型设备上仍有隐患;而静态内部类方案完全交给 JVM 保证,更“省心”。
使用场景限制:DCL 可以配合参数构造(比如传入配置对象),静态内部类做不到——因为 INSTANCE 是编译期确定的常量表达式,不能带运行时参数。
- 需要无参构造、纯内存态单例 → 优先选静态内部类
- 需要根据配置动态决定实例行为(如不同环境用不同实现)→ DCL 或枚举更合适
- Android 低版本(4.0 以前)对类初始化顺序有 bug,曾导致静态内部类失效,现基本可忽略
容易被忽略的兼容性坑
最隐蔽的问题是:当单例类实现了 Serializable,反序列化会生成新对象,破坏单例。静态内部类方案对此毫无防护,必须手动补 readResolve()。
另一个坑是模块化(Java 9+ Module System):如果 Singleton 在 module A,而 SingletonHolder 被 module B 的类意外引用(比如日志框架扫描所有类),可能触发提前初始化。
- 必须加上
private Object readResolve() { return getInstance(); } - 模块声明中避免
opens或exports整个包给不信任的模块 - 单元测试里别用
ClassLoader.loadClass("xxx.Singleton$SingletonHolder"),这会绕过 JVM 的懒加载保护
真正难的不是写对这十几行代码,而是想清楚“这个单例是否会被反射、序列化、模块系统、热部署工具干扰”——这些地方一漏,线程安全就只是假象。










