双重检查锁定不安全因new非原子操作且jvm可能重排序,导致返回未初始化对象;加volatile可禁止重排序并保证可见性;静态内部类利用类加载机制实现线程安全懒汉式;枚举单例由jvm保障唯一性,天然防反射和反序列化攻击;synchronized(this)无效因实例尚未创建。

双重检查锁定为什么 still 不安全?
因为 instance = new Singleton() 不是原子操作,JVM 可能重排序:分配内存 → 初始化对象 → 赋值引用。线程 A 执行到赋值但尚未初始化完成时,线程 B 进入 if (instance == null) 判断,发现非空就直接返回未初始化的 instance,触发 NullPointerException 或诡异状态。
修复方式只有一条硬规则:private static volatile Singleton instance; —— volatile 禁止指令重排序,并保证可见性。
- 不加
volatile,即使有双重检查,仍可能返回半初始化对象 -
synchronized方法块或整个getInstance()虽安全但性能差,失去懒汉意义 - JDK 5+ 才真正支持
volatile的禁止重排序语义,老 JVM 不行
静态内部类方式为什么线程安全且高效?
利用 JVM 类加载机制的天然同步性:外部类加载时,SingletonHolder 不被加载;首次调用 getInstance() 触发 SingletonHolder.class 加载,JVM 保证该过程单次、线程安全、且自动加锁。
示例关键片段:
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
- 无显式同步,无
volatile,无竞态,懒加载,性能接近直接访问静态字段 - 依赖 JVM 规范对“类初始化”阶段的严格定义,不是 Java 语言层的技巧
- 不能用于需要传参初始化的场景(构造函数带参数)
枚举单例为何能防反射和反序列化攻击?
普通单例在反射调用 Constructor.setAccessible(true) 后可绕过私有构造器创建新实例;反序列化时若未重写 readResolve(),也会生成第二个对象。而枚举类型由 JVM 保障实例唯一性,Enum 构造器禁止反射调用,且默认实现 readResolve 返回原有实例。
- 反射尝试
SingletonEnum.class.getDeclaredConstructor().newInstance()会抛出java.lang.NoSuchMethodException - 反序列化任意字节流得到的仍是同一个枚举常量,无需额外防御代码
- 缺点:无法继承、不能实现延迟初始化(所有枚举实例在类加载时即创建)
为什么 synchronized(this) 在单例里完全无效?
因为 this 是实例对象,而单例还没创建出来 —— 第一次调用 getInstance() 时 this 根本不存在。常见错误写法是把 synchronized 加在非静态方法上,等价于 synchronized(this),此时锁的是将要被创建的实例,毫无意义。
- 必须锁类对象:
synchronized(Singleton.class)或静态同步方法 - 锁实例对象在单例场景下等于没锁,多个线程会同时进入临界区
- IDE 通常会警告 “synchronization on local variable”,就是这个原因
volatile 的支持、枚举的特殊地位。面试时如果只背代码不讲清楚这些,漏洞就藏在你张嘴的第一句话里。Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










