以下是为你精心撰写的 《Java 单例模式深度学习指南》,专为 Java 后端开发者设计,内容涵盖:定义、作用、实际场景、实现方式对比、避坑指南、最佳实践、Spring 中的单例、面试高频题,所有示例均含中文注释,可直接用于项目开发与面试准备。
📘 Java 单例模式深度学习指南
—— 从零掌握“全局唯一实例”的设计艺术
作者:Java 后端架构实战导师
适用对象:Java 后端开发者(Spring Boot / 微服务 / 分布式系统)
目标:彻底理解单例模式,不再“只会写代码”,而是“懂原理、会选型、能避坑”
核心原则:不是所有全局变量都叫单例,不是所有单例都线程安全
✅ 一、什么是单例模式?
📌 定义:
单例模式(Singleton Pattern) 是一种创建型设计模式,它确保一个类在整个 JVM(Java 虚拟机)生命周期中只有一个实例,并提供一个全局访问点来获取这个唯一实例。
🔍 核心思想:
- 唯一性:整个系统中,该类只能被创建一次。
- 懒惰/立即创建:可以选择在类加载时创建,或在第一次使用时创建。
- 全局访问:提供一个静态方法(如
getInstance())让任何地方都能获取该实例。
💡 一句话记忆:
“一个类,一个对象,全局可访问。”
✅ 二、单例模式有什么作用?(Why Use Singleton?)
| 作用 | 说明 |
|---|---|
| ✅ 节省资源 | 避免重复创建开销大的对象(如数据库连接、线程池、Redis 客户端) |
| ✅ 全局共享状态 | 所有模块操作的是同一个对象,保证数据一致性(如配置中心、日志器) |
| ✅ 控制实例数量 | 防止滥用资源(如文件句柄、网络连接) |
| ✅ 简化配置管理 | 配置信息只需加载一次,避免重复读取配置文件 |
| ✅ 提高性能 | 减少对象创建与垃圾回收压力 |
⚠️ 注意:单例不是“全局变量”!
- 全局变量是
public static字段,无封装、无控制。- 单例是封装+控制+延迟创建的完整设计模式。
✅ 三、单例模式的典型使用场景(Java 后端真实案例)
| 场景 | 说明 | 是否推荐使用单例 |
|---|---|---|
| ✅ 数据库连接池 | HikariCP、Druid 等连接池必须唯一,避免连接数爆炸 | ✅ 强烈推荐 |
| ✅ Redis 客户端 | Jedis、Lettuce 客户端应全局共享,避免频繁创建 TCP 连接 | ✅ 推荐 |
| ✅ 日志记录器 | SLF4J 的 LoggerFactory.getLogger() 本质是单例 | ✅ 推荐 |
| ✅ 配置管理器 | 读取 application.yml 并缓存配置,避免重复解析 | ✅ 推荐 |
| ✅ UUID 生成器 | 使用 java.util.UUID.randomUUID() 无需单例,但自定义生成器可封装 | ⚠️ 可选 |
| ✅ 系统时间服务 | System.currentTimeMillis() 无需封装,但业务时间戳生成器可封装 | ⚠️ 按需 |
❌ 工具类(如 StringUtils) | 无状态,无需实例,静态方法即可 | ❌ 不推荐 |
❌ 业务实体(如 User、Order) | 每个用户/订单都应独立 | ❌ 绝对禁止 |
✅ 判断标准:
“这个对象是否昂贵?是否需要共享状态?是否只能有一个?”
→ 三者都满足 → 用单例!
✅ 四、单例模式的五种实现方式详解(含中文注释)
我们从最简单到最严谨,逐步分析每种写法的优缺点。
🔹 1. 饿汉式(Eager Initialization)—— 类加载时创建
/**
* 饿汉式单例(推荐用于简单场景)
* 特点:类加载时就创建实例,线程安全,但可能浪费内存
*/
public class DatabaseConnectionPool {
// 1. 私有静态实例:JVM 类加载时自动创建(线程安全)
private static final DatabaseConnectionPool INSTANCE = new DatabaseConnectionPool();
// 2. 私有构造函数:禁止外部 new
private DatabaseConnectionPool() {
System.out.println("✅ 数据库连接池已初始化(饿汉式)");
// 可在此初始化连接池、加载配置等
}
// 3. 公共静态方法:提供全局访问点
public static DatabaseConnectionPool getInstance() {
return INSTANCE; // 直接返回,无需同步
}
// 模拟获取连接
public void getConnection() {
System.out.println("🔗 获取数据库连接...");
}
}
// 使用示例
public class EagerSingletonDemo {
public static void main(String[] args) {
DatabaseConnectionPool pool1 = DatabaseConnectionPool.getInstance();
DatabaseConnectionPool pool2 = DatabaseConnectionPool.getInstance();
System.out.println(pool1 == pool2); // true,证明是同一对象
pool1.getConnection(); // 输出:🔗 获取数据库连接...
}
}
✅ 优点:
- 实现简单,线程安全(JVM 类加载机制保证)
- 获取速度快(无锁)
❌ 缺点:
- 类加载即创建,即使从未使用,也会占用内存(浪费资源)
- 无法控制初始化时机
💡 适用场景:
对象创建成本低、必用、启动即需:如日志器、配置中心、系统监控器
🔹 2. 懒汉式(Lazy Initialization)—— 第一次使用时创建(线程不安全版)
/**
* 懒汉式单例(线程不安全版本)—— ❌ 错误示范!
* 特点:延迟创建,节省资源,但多线程下会创建多个实例
*/
public class BadSingleton {
// 1. 声明静态实例,初始为 null
private static BadSingleton instance;
// 2. 私有构造函数
private BadSingleton() {
System.out.println("✅ 懒汉式单例被创建");
}
// 3. 公共静态方法:首次调用时才创建
public static BadSingleton getInstance() {
if (instance == null) { // 👈 多线程同时进入这里,会创建多个实例!
instance = new BadSingleton();
}
return instance;
}
}
// 使用示例(多线程测试)
public class BadSingletonDemo {
public static void main(String[] args) {
// 模拟10个线程同时获取实例
for (int i = 0; i < 10; i++) {
new Thread(() -> {
BadSingleton s = BadSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + " -> " + s);
}).start();
}
// 输出:可能看到多个不同对象!严重错误!
}
}
❌ 问题:
线程不安全! 在高并发下,多个线程同时进入
if (instance == null),会创建多个实例 → 破坏单例!
⚠️ 结论:
绝对不要在生产环境中使用此版本!
🔹 3. 懒汉式 + 同步方法(Synchronized Method)—— 线程安全但性能差
/**
* 懒汉式 + 同步方法(线程安全,但效率低)
* 特点:每次调用 getInstance() 都加锁,即使实例已创建
*/
public class SynchronizedSingleton {
private static SynchronizedSingleton instance;
private SynchronizedSingleton() {
System.out.println("✅ 懒汉式 + 同步方法被创建");
}
// 1. 整个方法加锁(synchronized)
public static synchronized SynchronizedSingleton getInstance() {
if (instance == null) {
instance = new SynchronizedSingleton();
}
return instance;
}
}
// 使用示例
public class SyncSingletonDemo {
public static void main(String[] args) {
// 多线程测试
for (int i = 0; i < 5; i++) {
new Thread(() -> {
SynchronizedSingleton s = SynchronizedSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + " -> " + s);
}).start();
}
// 输出:只会创建一次,但每个线程都阻塞等待锁
}
}
✅ 优点:
- 线程安全,解决了多线程创建多个实例的问题
❌ 缺点:
- 性能极差:每次调用都加锁,即使实例已创建,也要排队等待
- 锁粒度太大,不符合“只在创建时加锁”的原则
💡 适用场景:
仅用于学习或低并发场景,生产环境不推荐
🔹 4. 双重检查锁定(Double-Checked Locking)—— 推荐写法(JDK 5+)
/**
* 双重检查锁定(DCL)单例 —— ✅ 推荐生产环境使用
* 特点:延迟创建 + 线程安全 + 性能高
*/
public class DCLSingleton {
// 1. 使用 volatile 关键字!防止指令重排序
private static volatile DCLSingleton instance;
private DCLSingleton() {
System.out.println("✅ 双重检查锁定单例被创建");
}
// 2. 双重检查 + 同步块
public static DCLSingleton getInstance() {
// 第一次检查:避免不必要的同步(提高性能)
if (instance == null) {
// 加锁:只在创建时同步
synchronized (DCLSingleton.class) {
// 第二次检查:防止多个线程同时通过第一次检查
if (instance == null) {
instance = new DCLSingleton();
}
}
}
return instance;
}
}
// 使用示例
public class DCLSingletonDemo {
public static void main(String[] args) {
// 多线程并发测试
for (int i = 0; i < 10; i++) {
new Thread(() -> {
DCLSingleton s = DCLSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + " -> " + s);
}).start();
}
// 输出:只会创建一次,且无性能损耗
}
}
✅ 优点:
- 延迟初始化:第一次调用才创建
- 线程安全:双重检查 +
synchronized - 高性能:创建后不再加锁
- 内存可见性:
volatile确保多线程看到最新值
⚠️ 关键点:必须加 volatile!
如果没有
volatile,JVM 可能发生指令重排序:
- 分配内存
- 调用构造函数
- 将引用赋值给
instance
→ 如果重排序为 1→3→2,其他线程可能拿到一个未初始化完成的对象 → 崩溃!
💡 适用场景:
高并发、延迟加载、资源昂贵:如 Redis 客户端、数据库连接池、消息队列连接器
✅ 这是 Java 8+ 后最推荐的手写单例方式!
🔹 5. 静态内部类(Static Inner Class)—— 最优雅的实现(推荐!)
/**
* 静态内部类单例 —— ✅ 最推荐的写法(无锁、延迟、线程安全)
* 利用 JVM 类加载机制保证线程安全
*/
public class StaticInnerSingleton {
// 1. 私有构造函数
private StaticInnerSingleton() {
System.out.println("✅ 静态内部类单例被创建");
}
// 2. 静态内部类:只有在调用 getInstance() 时才会加载
private static class SingletonHolder {
private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
}
// 3. 获取实例:调用时才加载内部类
public static StaticInnerSingleton getInstance() {
return SingletonHolder.INSTANCE; // JVM 保证线程安全
}
}
// 使用示例
public class StaticInnerSingletonDemo {
public static void main(String[] args) {
// 多线程并发测试
for (int i = 0; i < 5; i++) {
new Thread(() -> {
StaticInnerSingleton s = StaticInnerSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + " -> " + s);
}).start();
}
// 输出:只会创建一次,且无锁、无延迟、线程安全
}
}
✅ 优点:
- 线程安全:JVM 类加载机制天然保证
- 延迟加载:第一次调用
getInstance()才加载内部类 - 无锁:性能极高
- 代码简洁:无
volatile、无synchronized - 符合 Java 最佳实践
💡 为什么安全?
Java 虚拟机规范规定:类的初始化是线程安全的。
SingletonHolder只有在getInstance()被调用时才会被加载和初始化,且由 JVM 保证只初始化一次。
✅ 结论:
这是目前最推荐的单例写法!
面试、项目、开源框架(如 Spring)中广泛使用!
🔹 6. 枚举单例(Enum Singleton)—— 最安全的实现(终极推荐)
/**
* 枚举单例 —— ✅ 最安全、防反射、防序列化、最推荐的终极方案
* Java 语言特性保证:枚举是天生的单例
*/
public enum EnumSingleton {
INSTANCE;
// 可添加业务方法
public void doSomething() {
System.out.println("✅ 枚举单例正在执行任务...");
}
// 可存储状态
private String config = "默认配置";
public String getConfig() {
return config;
}
public void setConfig(String config) {
this.config = config;
}
}
// 使用示例
public class EnumSingletonDemo {
public static void main(String[] args) {
// 获取实例
EnumSingleton singleton = EnumSingleton.INSTANCE;
// 调用方法
singleton.doSomething(); // ✅ 枚举单例正在执行任务...
// 修改状态
singleton.setConfig("新配置");
System.out.println("当前配置:" + singleton.getConfig());
// 多线程测试
for (int i = 0; i < 3; i++) {
new Thread(() -> {
EnumSingleton s = EnumSingleton.INSTANCE;
System.out.println(Thread.currentThread().getName() + " -> " + s);
}).start();
}
}
}
✅ 优点:
- 绝对线程安全
- 防止反射攻击(
Enum构造器是私有的,反射也无法创建新实例) - 防止序列化破坏(
ObjectInputStream读取枚举时会返回原实例) - 代码最简洁
- JVM 保证唯一性
❌ 缺点:
- 不支持延迟加载(类加载时就创建)
- 无法继承(枚举不能继承其他类)
💡 适用场景:
要求绝对安全、防破解、防反序列化:如安全令牌、加密服务、支付网关配置
✅ Joshua Bloch(《Effective Java》作者)推荐:优先使用枚举单例!
✅ 五、单例模式的避坑指南(Java 后端高频踩坑点)
| 问题 | 原因 | 解决方案 |
|---|---|---|
| ❌ 反射破坏单例 | 使用 Constructor.setAccessible(true) 创建新实例 | ✅ 使用枚举单例,或在构造器中抛异常:if (instance != null) throw new RuntimeException("禁止反射创建!") |
| ❌ 序列化破坏单例 | ObjectInputStream.readObject() 会创建新对象 | ✅ 实现 readResolve() 方法返回原实例 |
| ❌ 多 ClassLoader 破坏单例 | Web 容器中多个应用加载同一个类 | ✅ 用 Spring 容器管理单例,避免手写单例 |
| ❌ 多 JVM 破坏单例 | 分布式系统中每个 JVM 都有独立实例 | ✅ 单例只在单 JVM 生效!分布式用 Redis / ZooKeeper 实现全局唯一 |
| ❌ 静态变量被误改 | public static 暴露实例 | ✅ 私有化 + getInstance() 封装 |
🔧 防反射破坏示例(非枚举写法):
public class SafeSingleton {
private static volatile SafeSingleton instance;
private SafeSingleton() {
// 防止反射创建
if (instance != null) {
throw new RuntimeException("请使用 getInstance() 方法获取实例!");
}
}
public static SafeSingleton getInstance() {
if (instance == null) {
synchronized (SafeSingleton.class) {
if (instance == null) {
instance = new SafeSingleton();
}
}
}
return instance;
}
// 防止序列化破坏
private Object readResolve() {
return getInstance();
}
}
✅ 六、Spring 框架中的单例模式(你每天都在用!)
在 Spring 中,默认所有 Bean 都是单例!
@Component // ✅ 默认作用域是 singleton
public class RedisClient {
private String host = "localhost";
public void connect() {
System.out.println("🚀 连接到 Redis:" + host);
}
}
@Service
public class OrderService {
@Autowired
private RedisClient redisClient; // 每次注入的都是同一个对象!
public void createOrder() {
redisClient.connect(); // 调用的是同一个实例
}
}
@Configuration
public class AppConfig {
@Bean
public DatabaseConnectionPool connectionPool() {
return new DatabaseConnectionPool(); // Spring 容器管理为单例
}
}
✅ Spring 单例 vs 手写单例:
维度 手写单例 Spring 单例 实例创建 类加载或首次调用 容器启动时创建 生命周期 JVM 生命周期 Spring 容器生命周期 管理方式 手动控制 IoC 容器自动管理 线程安全 需自己保证 Spring 保证 推荐度 仅用于工具类 ✅ 业务 Bean 优先用 Spring
💡 建议:
业务对象用@Component/@Service,工具类用枚举单例,避免手写 DCL!
✅ 七、学习建议与进阶路径
| 阶段 | 建议 |
|---|---|
| 📚 第一周 | 用 EnumSingleton 替换你项目中所有手写单例(如配置类、工具类) |
| 📚 第二周 | 理解 volatile 为什么在 DCL 中必须使用,查阅 Java 内存模型(JMM) |
| 📚 第三周 | 在 Spring Boot 中,观察 @Component Bean 的创建过程(日志:BeanFactory) |
| 📚 第四周 | 尝试用 @Scope("prototype") 改为原型模式,对比差异 |
| 📚 面试准备 | 准备回答: |
“你项目中哪里用了单例?”
“手写一个线程安全的单例?”
“为什么用枚举?”
“Spring 中的单例和手写单例区别?” |
✅ 八、总结:单例模式选型决策树
graph TD
A[需要单例吗?] --> B{是否要求绝对安全?}
B -->|是| C[使用枚举单例(EnumSingleton)]
B -->|否| D{是否需要延迟加载?}
D -->|是| E[使用静态内部类(StaticInnerSingleton)]
D -->|否| F[使用饿汉式(EagerSingleton)]
G[是否在 Spring 项目中?] --> H[用 @Component / @Service,让 Spring 管理]
✅ 最终推荐:
- 普通工具类 → 枚举单例(最安全)
- Spring 项目中 → @Component(最简单)
- 非 Spring、高并发、延迟加载 → 静态内部类
- 绝对不要用懒汉式(无锁)或同步方法
✅ 九、附录:单例模式面试高频题(附答案)
Q1:单例模式有几种写法?推荐哪一种?
A:5种(饿汉、懒汉、同步方法、DCL、静态内部类、枚举),推荐枚举和静态内部类。
Q2:为什么 DCL 要加 volatile?
A:防止指令重排序导致返回未初始化对象,
volatile保证可见性和有序性。
Q3:Spring 中的单例是线程安全的吗?
A:是,Spring 的 Bean 默认是单例,容器保证线程安全(但 Bean 本身如果包含可变状态,仍需线程安全设计)。
Q4:单例能被反射破坏吗?如何防止?
A:能。在构造器中加判断
if (instance != null) throw new RuntimeException(),或使用枚举。
Q5:分布式系统中能用单例吗?
A:不能!单例只在 JVM 内部唯一。分布式需用 Redis、ZooKeeper 实现全局唯一锁或配置中心。
✅ 十、结语:真正的高手,不写单例,而用 Spring
单例不是目的,而是手段。
你真正要解决的是:“如何让一个昂贵对象在整个系统中被共享且只创建一次?”
✅ 当你使用
@Autowired RedisClient时,你已经在用单例了。
✅ 当你用EnumSingleton管理加密密钥时,你已经做到了极致安全。
不要为了“写单例”而写单例,
要为了“系统稳定、性能高效、资源可控”而选择它。
96

被折叠的 条评论
为什么被折叠?



