一、单例模式:全局配置管理器
单例模式,说白了就是——一个类只能有一个实例。
你想想看,有些东西在系统里只需要一份就够了。比如日志记录器、数据库连接池、Spring 里的 Bean 默认作用域。如果每个地方都 new 一个,那不乱套了?
我记得刚入行那会儿,有个同事写了个日志工具类,每次调用都 new 一个实例。结果线上日志文件被多个线程同时写,内容全乱了。后来改成单例,问题就解决了。
嗯,这就是单例模式的价值所在。
1.1 什么是单例模式?
单例模式确保一个类只有一个实例,并提供一个全局访问点。
它的核心要点就两个:
- 私有化构造方法——不让外部 new
- 提供一个静态方法——返回唯一的实例
听起来简单吧?但实现起来有不少坑。
1.2 业务场景:日志记录器
日志记录器是最典型的单例应用。为什么?
因为日志文件只有一个。如果每个模块都自己写日志,文件锁、写入顺序、性能都会出问题。
来看一个简单的实现:
public class Logger {
// 私有静态实例
private static Logger instance;
// 私有构造方法
private Logger() {
// 初始化日志文件
}
// 全局访问点
public static Logger getInstance() {
if (instance == null) {
instance = new Logger();
}
return instance;
}
public void log(String message) {
// 写入日志文件
System.out.println("[LOG] " + message);
}
}
这个写法叫懒汉式——用到的时候才创建。
但有个问题:多线程环境下,两个线程同时调用 getInstance(),可能会创建两个实例。
1.3 业务场景:数据库连接池
数据库连接池也是单例的经典场景。
你想想看,如果每个请求都创建一个连接池,那数据库很快就会被拖垮。连接池应该全局唯一,统一管理连接的分配和回收。
我个人习惯用双重检查锁来实现:
public class ConnectionPool {
// volatile 防止指令重排序
private static volatile ConnectionPool instance;
private ConnectionPool() {
// 初始化连接池
}
public static ConnectionPool getInstance() {
if (instance == null) {
synchronized (ConnectionPool.class) {
if (instance == null) {
instance = new ConnectionPool();
}
}
}
return instance;
}
}
这里有个细节:volatile 关键字不能少。为什么?
因为 instance = new ConnectionPool() 不是原子操作。JVM 可能会先分配内存,再赋值给 instance,最后才初始化对象。如果另一个线程在赋值后、初始化前访问,就会拿到一个半成品。
1.4 业务场景:Spring Bean 默认作用域
Spring 的 Bean 默认就是单例的。这也是为什么我们通常不需要手动实现单例模式。
Spring 容器会保证每个 Bean 只有一个实例。你只管注入,不用操心创建和销毁。
但要注意:有状态的 Bean 不适合单例。
比如一个计数器 Bean,如果多个线程同时修改它的状态,就会出现数据不一致。
| 作用域 | 说明 | 适用场景 |
|---|---|---|
| singleton | 全局唯一实例 | 无状态服务、工具类 |
| prototype | 每次注入都创建新实例 | 有状态对象、用户会话 |
| request | 每个 HTTP 请求一个实例 | Web 应用中的请求数据 |
| session | 每个用户会话一个实例 | 用户登录信息 |
我在项目中遇到过一个问题:一个 Service 里有个 List 成员变量,用来缓存数据。结果线上多个用户的数据互相覆盖了。排查下来,就是因为 Service 是单例的,List 被所有线程共享。
解决方案很简单:要么把 List 改成局部变量,要么把 Service 改成 prototype 作用域。
1.5 单例模式的几种实现方式对比
这里我整理了一张对比表,方便你选型:
| 实现方式 | 线程安全 | 延迟加载 | 推荐指数 |
|---|---|---|---|
| 饿汉式 | ✅ 安全 | ❌ 否 | ⭐⭐⭐ |
| 懒汉式(不加锁) | ❌ 不安全 | ✅ 是 | ⭐ |
| 双重检查锁 | ✅ 安全 | ✅ 是 | ⭐⭐⭐⭐ |
| 静态内部类 | ✅ 安全 | ✅ 是 | ⭐⭐⭐⭐⭐ |
| 枚举 | ✅ 安全 | ❌ 否 | ⭐⭐⭐⭐ |
我个人最推荐静态内部类的方式。它既实现了延迟加载,又保证了线程安全,代码还简洁:
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
为什么这种方式是线程安全的?
因为 JVM 在加载内部类时会加锁,保证了 INSTANCE 只会被初始化一次。
1.6 单例模式的核心逻辑
下面这张图展示了单例模式的核心流程:
1.7 避坑指南
单例模式看着简单,但坑不少。我总结几个常见的:
- 序列化破坏单例——如果单例类实现了 Serializable,反序列化时会创建新实例。解决方案是添加
readResolve()方法。 - 反射破坏单例——通过反射调用私有构造方法,可以创建多个实例。可以在构造方法中加个标志位来防御。
- 类加载器不同——不同类加载器加载同一个类,会生成不同的实例。这在 Web 容器中要特别注意。
核心要点回顾:
- 单例模式保证一个类只有一个实例
- 适用于日志记录器、连接池、配置管理等场景
- 实现方式有多种,静态内部类最推荐
- 注意线程安全、序列化、反射等潜在问题
好了,单例模式就讲到这里。记住一句话:能用 Spring 单例就别自己写,省心又安全。