单例模式是Java开发中最常用、面试最高频的设计模式之一,它看似简单,实则蕴含深厚的并发编程底层原理,并且在Spring、Servlet等主流框架中有着广泛的落地应用。
一、单例模式与设计原则
1 单例模式:
一个类在 JVM 运行期间,有且仅有一个实例对象。
- 生活类比:世界上只有一个中国,无论何人询问,指向的都是同一个主体
- 程序场景:配置管理器、数据库连接池、线程池、日志工具类,全局仅需一个实例即可支撑全流程操作
2 单例模式两大要求
- 私有构造方法
- 公共静态获取实例方法。
二、单例模式实现方案示例
1 饿汉式单例(简单、线程安全)
类加载时立即初始化实例,心急如饿汉,无需等待使用时创建。
java
class HungrySingleton {
// 类加载时创建实例,final保证引用不可二次赋值
private static final HungrySingleton INSTANCE = new HungrySingleton();
// 私有构造,禁止外部实例化
private HungrySingleton() {}
// 公共获取方法
public static HungrySingleton getInstance() {
return INSTANCE;
}
}
优缺点分析
✅ 优点:实现极简,依靠 JVM 类加载的线程安全性,天然无并发问题
❌ 缺点:非懒加载,类加载即创建实例,若实例长期未使用,会造成内存浪费
2 双重校验锁(DCL)懒汉式
懒汉式核心:懒加载,首次使用时才创建实例,节约内存资源 。DCL(Double Check Lock)是懒汉式的最优实现,融合双重判空 + 类锁 + volatile,兼顾线程安全与高性能。
java
class DclSingleton {
// volatile:禁止指令重排序,解决半初始化对象问题
private volatile static DclSingleton INSTANCE;
private DclSingleton() {}
public static DclSingleton getInstance() {
// 第一层判空:避免每次获取都加锁,大幅提升性能
if (INSTANCE == null) {
// 类锁:保证多线程下实例创建的原子性
synchronized (DclSingleton.class) {
// 第二层判空:防止多线程同时突破第一层判空后重复创建
if (INSTANCE == null) {
INSTANCE = new DclSingleton();
}
}
}
return INSTANCE;
}
}
volatile 的必要性(复习)
new DclSingleton()在 JVM 中分三步执行:
- 分配对象内存空间
- 初始化对象属性
- 将
INSTANCE引用指向内存空间JVM 可能对指令重排序,执行顺序变为
1→3→2。若线程 A 执行完步骤 3,对象尚未初始化,线程 B 判断INSTANCE!=null,会获取到半初始化的异常对象。
volatile的作用:禁止指令重排序,保证实例完全初始化后,其他线程才能读取到引用。
三、单例模式与并发关键字关联
| 知识点 | 在单例模式中的作用 |
|---|---|
| final(饿汉式) | 修饰实例引用,保证INSTANCE指向唯一对象,禁止二次赋值 |
| volatile(DCL) | 禁止new指令重排序,杜绝半初始化对象问题 |
| 指令重排序 | DCL 必须加 volatile 的核心原因,与 final 内存语义底层逻辑一致 |
四、单例模式在 Servlet/Spring Controller 中的应用
1 场景
Servlet、Spring MVC 的Controller默认采用单例作用域,是单例模式在 Web 开发中经典的落地场景。
2 为什么 Servlet/Controller 要设计为单例?
方法调用底层原理 非静态方法必须依托对象调用,但线程调用方法时,会将方法栈帧拷贝到自身线程栈独立执行,不同线程的栈帧互不干扰,方法层面天然线程安全。
单例的内存价值
反例:1 万个请求→创建 1 万个 Servlet 对象→频繁创建销毁,加重 GC 压力,严重浪费内存
正例:全局仅 1 个对象→所有线程共享对象,拷贝方法执行→内存利用率最大化
3 内存可视化示例

4 单例安全的前提
Servlet/Controller 单例安全的关键是无状态设计:
- 类中不定义实例变量、共享静态变量,仅做请求处理、逻辑转发
- 若定义共享成员变量,多线程并发修改会引发线程安全问题
Servlet、Controller 这类无状态的请求处理类,采用单例模式,全局仅创建一个对象,所有请求线程共享该对象并独立拷贝方法栈帧执行,既满足非静态方法的调用要求,又保证线程安全,同时大幅节省内存与 GC 资源。
五、枚举单例深度剖析(最安全但有坑)
1 枚举单例实现(代码极简)
枚举是《Effective Java》推荐的单例实现,JVM 底层天然保证单例。
java
// 枚举单例:JVM保证唯一实例
enum ServletSingleton {
// 全局唯一的枚举实例
INSTANCE;
// 业务方法(对应Servlet的doGet/doPost)
public void doGet() {
System.out.println("处理GET请求");
}
public void doPost() {
System.out.println("处理POST请求");
}
}
2 枚举单例的优势
1 JVM 天然线程安全:枚举实例在类加载时由 JVM 唯一创建,无并发问题
2 防反射、防序列化破坏:普通单例可通过反射调用私有构造、反序列化创建新对象,枚举从底层杜绝该漏洞
3 代码简单:无需手动处理锁、volatile,开发成本低
3 枚举单例的缺点
饿汉式加载,无法懒加载:类加载时即创建实例,无论是否使用;
启动性能损耗:若项目中有大量枚举单例,启动时会一次性创建所有实例,拖慢启动速度,浪费内存资源。
4 枚举单例 VS DCL 单例
| 特性 | 枚举单例 | DCL 单例 |
|---|---|---|
| 实现难度 | 极简 | 稍复杂(双判空 + volatile + 类锁) |
| 线程安全 | JVM 天然保证 | 手动编码保证 |
| 加载时机 | 类加载时(饿汉) | 首次调用时(懒汉) |
| 内存占用 | 启动时提前占用 | 按需创建,更节省 |
| 防反射 / 序列化 | 天然支持 | 需额外编码处理 |
| 适用场景 | 简单、内存不敏感场景 | 高并发、内存敏感的企业级 Web 项目 |
枚举单例是 JVM 层面最安全的单例实现,天然防反射、防序列化破坏,代码简洁,但属于饿汉式加载无法懒加载,适合轻量场景
DCL 懒汉式支持懒加载、性能优异,更适合高并发、内存敏感的生产级 Web 项目
六、要点总结
- 单例本质 :一个类全局仅有一个实例,依靠私有构造 + 公共静态获取方法实现;、
- 经典实现选型 :饿汉式简单安全但非懒加载,DCL 懒汉式是面试最优解,
volatile是核心 - 底层关联:volatile 禁止指令重排序,解决半初始化对象问题,与 final 内存语义同源
- Web 实战:Servlet/Controller 默认单例,无状态设计保证线程安全,是内存优化的关键
- 枚举单例:最安全但无懒加载,与 DCL 单例场景互补,需根据业务按需选择