Appearance
singleton-dcl
1. 实验目标与工程要点
本实验对比 Java 单例模式的 5 种常见实现方式(错误懒汉、方法同步、DCL + volatile、静态内部类、枚举单例),重点探讨安全发布 (Safe Publication)、懒加载 (Lazy Initialization) 以及指令重排序带来的“半初始化对象”风险。
2. 深入原理与生活浅显比喻
2.1 生活浅显比喻:建房子与钥匙交付
假设你要找装修公司盖一座房子(创建单例对象):
没有
volatile的 DCL 风险:- 装修公司刚划好了地皮(分配了内存空间);
- 交付人员就把门钥匙给了你(将非空引用赋值给静态变量
instance); - 但此时房子内部的水电和家具还没装完(构造函数还没初始化完成!);
- 你拿着钥匙推门进去用卫生间,直接摔坑里(拿到半初始化对象,抛出
NullPointerException或数据损坏)。
加了
volatile的 DCL 防护:- 硬件通过内存屏障 (Memory Barrier) 约束:必须等房子装修完全完毕(构造函数完成)之后,才允许把门钥匙交给客户(将引用写入主内存)。
2.2 5 种单例实现方式对比矩阵
| 单例实现方式 | 懒加载 | 线程安全性 | 吞吐性能 | 运行开销 | 核心原理解析与适用场景 |
|---|---|---|---|---|---|
SingletonDcl (DCL + volatile) | ✅ 属于 | ✅ 安全 | 🚀 较高(仅首次加锁) | 📉 极低 | 双重检查锁:外层 if 避免已创建后的锁开销,内层 if 保障单例,volatile 阻止指令重排序。 |
EnumSingleton (枚举单例) | ❌ 否 | ✅ 绝安全 | ⚡ 极高(无锁) | 📉 极低 | JVM 规范保证:天然防止反射破坏私有构造函数,防止反序列化重新创建对象。推荐。 |
InnerClassSingleton (静态内部类) | ✅ 属于 | ✅ 安全 | ⚡ 极高(无锁) | 📉 极低 | 类加载器机制:依赖 JVM 类加载阶段的 <clinit> 互斥锁实现懒加载与线程安全。代码精简。 |
SynchronizedSingleton (同步方法) | ✅ 属于 | ✅ 安全 | 🐢 较差(每次都加锁) | 📈 较高(方法级锁阻塞) | 方法加锁:public synchronized static getInstance()。每次获取都竞争类锁,吞吐极低,工程不推荐。 |
BadSingleton (非线程安全) | ✅ 属于 | ❌ 危险 | ⚡ 极高 | 📉 极低 | 无同步控制:多线程同时进入 if (instance == null) 时会重复创建多个实例,破坏单例契约。 |
3. Android / 移动端实战应用场景与事故注意事项
3.1 Android 实战场景(选型速查)
| 需求 | 推荐实现 | 说明 |
|---|---|---|
| 无参、懒加载、日常业务单例 | 静态内部类 InnerClassSingleton | Android/Java 最常见;不抢过早类加载 |
| 无参、极简、防反射/序列化 | enum EnumSingleton | 类加载即创建;Kotlin 项目更常 object |
| 构造要参数 / Application 里 init | DCL + volatile SingletonDcl | 显式控制首次创建时机 |
| 教学对照、理解加锁代价 | SynchronizedSingleton | 能工作,高频 get 不推荐 |
| 生产代码 | ❌ BadSingleton | 仅反例 |
全局管理类(网络门面、图片协调等):优先 Holder 或 Hilt @Singleton;持 Context 必须用 applicationContext,与单例写法无关。
3.2 常见误配与内存泄漏事故
- ⚠️ DCL 遗漏
volatile关键字- 后果:在高并发高吞吐场景下,极小概率导致线程读取到未完成
init的半成品对象,引发致命 Crash。
- 后果:在高并发高吞吐场景下,极小概率导致线程读取到未完成
- ⚠️ Android 单例持有
Context导致 Activity 内存泄漏- 后果:单例的生命周期与 App 进程同长。如果传入
Activity的Context,会导致 Activity 销毁后无法被 GC 回收,造成严重内存泄漏! - 正确修法:单例内部必须统一强制使用
context.applicationContext。
- 后果:单例的生命周期与 App 进程同长。如果传入
4. 实验源码与运行验证
关键源码(特点与使用场景)
各文件类注释含完整说明;下表为速查。
| 源码 | 懒加载 | 线程安全 | 特点摘要 | Java 适用 | Android 适用 |
|---|---|---|---|---|---|
| BadSingleton.java | ✅ | ❌ | 无同步懒汉,单线程表象正常 | 仅反例,禁止生产 | 多线程/协程下同样危险 |
| SynchronizedSingleton.java | ✅ | ✅ | 方法级 synchronized,每次 get 都争锁 | 低并发、教学 | 可用但不如 Holder;高频不推荐 |
| SingletonDcl.java | ✅ | ✅ | 双重检查 + volatile,仅首次加锁 | 懒加载 + 传参 / 高并发 get | Application 按配置 init 的管理器 |
| InnerClassSingleton.java | ✅ | ✅ | Holder 类加载初始化,无锁、写法短 | 无参单例首选之一 | 无/仅 Application Context 的全局类 |
| EnumSingleton.java | ❌ | ✅ | JVM 保证唯一,防反射/反序列化 | 无参、库代码、Effective Java 推荐 | 简单无 Context 工具;复杂 init 用 Holder/DI |
工程默认顺序(无参单例):静态内部类 ≈ enum > DCL(需传参或显式 init 时)> 方法同步 > 错误懒汉。
- 双重检查锁:SingletonDcl.java
- 枚举单例:EnumSingleton.java
- 静态内部类单例:InnerClassSingleton.java
- 方法同步单例:SynchronizedSingleton.java
- 错误示例(反例):BadSingleton.java
运行方式
bash
cd labs/java/runtime-concurrency/singleton-dcl
javac -d out src/*.java
java -cp out SingletonDcl预期控制台运行输出
text
实例 A 内存地址: SingletonDcl@8807e25
实例 B 内存地址: SingletonDcl@8807e25
是否为同一单例对象 (a == b): true5. 对应知识库文档
- 理论主文档:Java 并发模型总览