Java设计模式-单例模式
模式概述
单例模式简介
核心思想:确保一个类在整个应用程序生命周期中仅创建一个实例,并提供一个全局访问点来获取该实例。通过私有化构造方法和控制实例化逻辑,单例模式避免了重复创建对象的开销,确保了全局状态的一致性。
模式类型:创建型模式(Creational Pattern)。
作用:
- 唯一实例控制:确保类仅存在一个实例,避免因多次实例化导致的资源浪费(如数据库连接池、配置管理器);
- 全局访问点:提供统一的全局访问入口,简化对象访问逻辑(如日志系统只需一个实例记录日志);
- 节省资源:对于高开销或需共享状态的对象(如线程池、缓存管理器),单例模式避免了重复初始化的资源消耗;
- 状态一致性:全局唯一实例确保所有客户端访问的是同一对象状态,避免因状态不一致导致的逻辑错误。
典型应用场景:
- 全局配置管理:如读取应用配置(
config.properties
)的ConfigManager
,只需一个实例加载配置; - 日志系统:
Logger
类需全局共享,避免多个实例重复写入日志文件; - 数据库连接池:
ConnectionPool
需统一管理连接,单例模式确保所有请求共享同一连接池; - 缓存管理器:
CacheManager
存储高频访问数据,单例模式避免重复创建缓存实例; - 设备驱动程序:如打印机驱动(
PrinterDriver
),系统中仅需一个实例控制硬件。
我认为:单例模式就像应用中的“唯一管家”,负责管理一个关键资源,确保所有需要它的地方都通过同一个入口获取,既避免了资源浪费,又保证了操作的一致性。
课程目标
- 理解单例模式的核心思想和经典应用场景
- 识别应用场景,使用单例模式解决功能要求
- 了解单例模式的优缺点
核心组件
角色-职责表
角色 | 职责 | 示例类名 |
---|---|---|
单例类(Singleton) | 私有化构造方法,控制实例创建逻辑,提供全局访问点获取唯一实例。 | ConfigManager (配置管理器)、Logger (日志记录器) |
类图
下面是一个简化的类图表示,展示了单例模式中的主要角色及其交互方式:
传统实现 VS 单例模式
案例需求
案例背景:设计一个日志记录器(Logger
),要求全局仅存在一个实例,所有模块通过该实例记录日志(如写入文件或控制台)。传统方式通过new Logger()
创建实例,导致多个日志实例重复写入文件,引发数据混乱。
传统实现(痛点版)
代码实现:
// 传统方式:无单例控制,可随意创建多个实例
public class Logger {
private String logFile; // 日志文件路径
// 公开构造方法(问题根源:允许外部任意创建实例)
public Logger(String logFile) {
this.logFile = logFile;
System.out.println("初始化日志文件:" + logFile);
}
// 日志记录方法
public void log(String message) {
System.out.println("写入日志[" + logFile + "]:" + message);
}
}
// 客户端使用(错误示范)
public class Client {
public static void main(String[] args) {
// 创建第一个日志实例(写入app.log)
Logger logger1 = new Logger("app.log");
logger1.log("用户登录");
// 创建第二个日志实例(重复初始化,写入error.log)
Logger logger2 = new Logger("error.log");
logger2.log("系统异常");
}
}
痛点总结:
- 实例泛滥:外部可通过
new
无限制创建实例,导致多个日志实例重复初始化(如重复打开文件),浪费资源; - 状态不一致:不同实例指向不同日志文件(如
app.log
和error.log
),客户端无法保证全局日志统一; - 数据混乱:多个实例同时写入不同文件,无法追踪完整的日志流(如用户登录和系统异常需关联分析)。
单例模式 实现(优雅版)
代码实现:
// 单例模式:Logger(线程安全、懒加载)
public class Logger {
// 1. 静态私有实例(volatile保证可见性和禁止指令重排)
private static volatile Logger instance;
// 2. 私有构造方法(禁止外部new)
private Logger(String logFile) {
System.out.println("初始化日志文件:" + logFile);
// 实际场景:加载配置、打开文件流等高开销操作(仅执行一次)
}
// 3. 全局访问方法(双重检查锁定保证线程安全)
public static Logger getInstance(String logFile) {
// 第一次检查:避免不必要的同步
if (instance == null) {
synchronized (Logger.class) { // 同步类对象
// 第二次检查:防止多线程同时通过第一次检查后重复创建
if (instance == null) {
instance = new Logger(logFile);
}
}
}
return instance;
}
// 日志记录方法
public void log(String message) {
System.out.println("写入日志[" + logFile + "]:" + message);
// 实际场景:写入文件或发送到日志服务
}
}
// 客户端使用(正确示范)
public class Client {
public static void main(String[] args) {
// 获取单例实例(仅初始化一次)
Logger logger1 = Logger.getInstance("app.log");
logger1.log("用户登录");
// 再次获取实例(返回已存在的实例)
Logger logger2 = Logger.getInstance("error.log"); // 注意:此处logFile会被忽略!
logger2.log("系统异常");
}
}
优化说明:
上述代码存在一个问题:第二次调用
getInstance("error.log")
时,由于instance
已存在,会直接返回第一次创建的app.log
实例,导致error.log
被忽略。因此,正确的单例模式需在首次调用时固定日志文件路径。以下是修正后的实现:public class Logger { private static volatile Logger instance; private final String logFile; // 关键:设为final,确保初始化后不可修改 // 私有构造方法(参数在首次调用时传入) private Logger(String logFile) { this.logFile = logFile; System.out.println("初始化日志文件:" + logFile); } // 全局访问方法(首次调用时指定logFile,后续调用忽略参数) public static Logger getInstance(String logFile) { if (instance == null) { synchronized (Logger.class) { if (instance == null) { instance = new Logger(logFile); // 仅首次调用时使用参数 } } } return instance; } public void log(String message) { System.out.println("写入日志[" + logFile + "]:" + message); } } // 客户端正确使用 public class Client { public static void main(String[] args) { // 首次调用指定日志文件(必须!否则instance为null时参数被忽略) Logger logger1 = Logger.getInstance("app.log"); logger1.log("用户登录"); // 后续调用无需关心日志文件(已被首次调用固定) Logger logger2 = Logger.getInstance("error.log"); logger2.log("系统异常"); // 实际写入app.log(因为instance已初始化) } }
优势:
- 唯一实例:通过私有构造和双重检查锁定,确保全局仅一个实例;
- 懒加载:实例在首次调用
getInstance()
时创建,避免类加载时立即初始化(节省资源); - 线程安全:
synchronized
同步类对象,防止多线程并发创建实例; - 高内聚:日志文件路径在首次初始化时固定,后续操作无需关心路径,避免状态混乱。
局限:
- 序列化问题:若单例类实现
Serializable
接口,反序列化时会创建新实例(破坏单例),需重写readResolve()
方法返回现有实例; - 反射攻击:通过反射调用私有构造方法(
setAccessible(true)
)可创建新实例,需通过异常处理或标记私有构造方法防御; - 多类加载器问题:不同类加载器加载同一单例类时,会创建多个实例(需限定类加载器范围)。
模式变体
变体 | 实现方式 | 特点 | 适用场景 |
---|---|---|---|
饿汉式 | 类加载时直接初始化实例(private static final Singleton instance = new Singleton(); ) |
简单、线程安全,但可能浪费资源(若实例未被使用) | 实例创建成本低、确定会被使用的场景 |
懒汉式(非线程安全) | 首次调用时创建实例(if (instance == null) instance = new Singleton(); ) |
简单但线程不安全(多线程可能创建多个实例) | 单线程环境或测试场景 |
双重检查锁定 | 结合volatile 和synchronized ,首次调用时加锁创建实例 |
线程安全、懒加载、高效(仅首次同步) | 生产环境主流实现 |
静态内部类 | 利用类加载机制,通过静态内部类持有实例(public static Singleton getInstance() { return Holder.INSTANCE; } ,private static class Holder { static final Singleton INSTANCE = new Singleton(); } ) |
线程安全、懒加载(内部类在首次调用时加载)、无同步开销 | 推荐替代双重检查锁定的简洁实现 |
枚举单例 | 通过枚举类型定义实例(public enum Singleton { INSTANCE; } ) |
绝对线程安全、防反射/反序列化攻击(JVM保证枚举实例唯一) | 需严格防破坏的单例场景 |
最佳实践
建议 | 理由 |
---|---|
优先使用静态内部类 | 代码简洁,利用JVM类加载机制实现线程安全的懒加载,无同步开销,是生产环境推荐方案。 |
枚举单例防破坏 | 若需严格防止反射攻击或反序列化破坏单例(如安全敏感场景),使用枚举类型(enum )。 |
私有构造方法防御反射 | 在私有构造方法中添加检查(如if (instance != null) throw new RuntimeException("单例实例已存在"); ),防止反射调用。 |
处理序列化问题 | 若单例类需序列化,重写readResolve() 方法(protected Object readResolve() throws ObjectStreamException { return getInstance(); } ),避免反序列化创建新实例。 |
明确初始化参数 | 首次调用getInstance() 时必须传入必要参数(如日志文件路径),后续调用忽略参数,确保实例状态正确。 |
避免滥用单例 | 仅当对象需全局唯一时使用单例模式,否则可能导致代码耦合(如将业务逻辑与单例绑定,难以测试)。 |
一句话总结
单例模式通过控制类仅创建一个实例并提供全局访问点,解决了资源浪费、状态不一致等问题,是实现全局唯一对象的高效方案。
如果关注Java设计模式内容,可以查阅作者的其他Java设计模式系列文章。😊