Java 中如何实现线程安全的单例?
简化版
Java 中常用的线程安全单例包括饿汉式、同步访问方法、双重检查锁、静态内部类和枚举。工程上优先选择语义清晰且不易写错的静态字段、静态内部类或枚举,并把“安全创建实例”和“实例内部状态线程安全”分开处理。
详细版
可以按是否需要懒加载选择实现:
- 不需要懒加载:使用
private static final静态字段; - 需要简单的懒加载:对整个
getInstance()使用synchronized; - 需要懒加载且减少稳定阶段的加锁:静态内部类或正确的双重检查锁;
- 需要较强的反射和序列化防护:单元素枚举。
同步方法是正确实现,并非“不能用”,只是每次获取都要经过监视器语义。双重检查锁必须把实例字段声明为 volatile。静态内部类借助 JVM 类初始化协议,只在内部类首次主动使用时创建实例。
无论采用哪种创建方案,若单例持有可变集合、计数器等共享状态,业务方法仍需单独保证原子性、可见性和不变式。
完整版教学
一、先定义线程安全目标
线程安全单例至少要满足:
- 并发获取时只完成一次有效初始化;
- 其他线程看到的是构造完成、正确发布的对象;
- 初始化失败不会悄悄返回半成品;
- 实例中的共享可变状态得到额外保护。
只用 if (instance == null) 无法满足前两项。
二、同步方法方案
public final class SafeLazyService {
private static SafeLazyService instance;
private SafeLazyService() {}
public static synchronized SafeLazyService getInstance() {
if (instance == null) {
instance = new SafeLazyService();
}
return instance;
}
}
类对象的监视器保证同一时刻只有一个线程执行获取方法,解锁与随后加锁之间还建立可见性关系。它容易审查,适用于获取频率不高或性能要求普通的场景。
三、静态字段与静态内部类
不要求懒加载时,静态字段最直接:
private static final SafeService INSTANCE = new SafeService();
要求懒加载时,可以把实例放进静态内部类:
private static class Holder {
private static final SafeService INSTANCE = new SafeService();
}
外部类初始化不会自动初始化 Holder;第一次读取 Holder.INSTANCE 时,JVM 同步完成内部类初始化。
四、双重检查锁的适用位置
双重检查锁通过第一次无锁读取走快速路径,只在实例尚未创建时进入同步块。它是正确方案,但代码比静态内部类更容易因遗漏 volatile 或错误同步对象而出问题。只有确实需要这种初始化控制时才值得使用。
五、对象创建安全不代表业务状态安全
public void add(String value) {
values.add(value);
}
如果 values 是普通 ArrayList,即使单例创建完全安全,并发调用 add 仍不安全。可以采用不可变数据、线程安全集合、锁或原子类,但具体方案必须围绕复合操作的不变式设计。
六、五种方案按初始化需求而不是背诵排序
线程安全单例没有唯一标准答案。先问是否需要懒加载、是否有动态参数、是否要抵抗原生序列化/标准反射,再选择最短且可证明的实现,通常比先写复杂 DCL 更可靠。
无需懒加载 -> static final 饿汉式
需要无参懒加载 -> Holder
需要固定枚举常量且重视序列化/反射 -> enum
简单懒加载且性能普通 -> synchronized 方法
确需锁外快速路径 -> volatile + DCL
需要运行时参数/重试/销毁 -> 容器或显式工厂
任何方案之后 -> 单独审查实例内部共享状态
这条时序必须同时检查“谁负责创建”和“其他线程或调用方从哪里取得实例”。只看到最终引用相同还不够;若构造副作用执行了两次、发布了半初始化状态,或者作用域边界之外又创建了一份,就已经违反了本题讨论的保证。
七、保证范围与失效边界
| 评审维度 | 本题结论 |
|---|---|
| 唯一性/正确性保证 | 正确方案同时保证一次有效创建与安全发布;具体机制可能是类初始化、监视器或 volatile。 |
| 作用域边界 | 创建线程安全只涵盖实例身份与初始状态,不涵盖业务方法中所有可变数据。 |
| 仍未解决的问题 | 朴素 null 检查、错误 DCL、构造期间 this 逸出以及多 ClassLoader 都会突破简单结论。 |
| 更合适的替代方案 | 优先可读且平台托管的 static final、Holder、enum;复杂生命周期交给 DI 容器。 |
设计模式的保证必须带作用域。把“某个类初始化边界只创建一次”说成“整个系统永远只有一个”,或把“安全发布引用”说成“对象所有方法都线程安全”,都会把不同层次的问题混在一起。
八、工程落地时的验证方法
- 为构造过程增加可观察计数,使用 32 个并发任务同时获取实例,构造计数应符合本题保证。
- 比较引用身份时使用
==,不要让重写后的equals()掩盖实际存在的多个对象。 - 若涉及类加载器,记录
instance.getClass().getClassLoader(),同时比较类对象和加载器身份。 - 若涉及序列化,至少执行一次“序列化 → 反序列化”,并核对返回引用及外部副作用。
- 若涉及 Spring,分别检查 Bean 名称、
ApplicationContext身份和作用域,而不只检查 Java 类名。 - 对共享可变字段另做并发测试;实例创建正确并不能替代业务状态的原子性测试。
- 对初始化失败、重复调用和资源关闭分别设计用例,确认生命周期语义可解释。
记忆钩子:先选初始化机制,再审业务状态;“安全拿到对象”和“安全使用对象”是两张检查表。
九、常见误区与追问
- 误区:同步方法是错误实现。 它是正确但每次获取带监视器语义的简单方案。
- 误区:单例字段声明 final 就能实现懒加载。 final 适合构造或类初始化赋值,动态懒加载仍需同步协议。
- 误区:实例创建安全等于 ArrayList 字段并发安全。 共享集合的复合操作必须单独保护。
- 追问:哪种方案通常最容易解释懒加载? 无参数场景下 Holder 借助独立类初始化,代码短且证明清晰。
- 追问:DCL 的 instance 为什么要 volatile? 让发布写与锁外读建立可见性和顺序保证。
- 追问:测试并发创建只比较返回对象够吗? 还应统计构造副作用次数并检查对象初始字段可见性。
十、加强记忆
线程安全单例要同时关注“只创建一次”和“安全发布”,业务状态还要另算。无需懒加载选静态字段,需要懒加载优先考虑静态内部类,枚举适合简单无继承需求的强约束单例,双重检查锁则必须与 volatile 配套。