设计模式之单例模式大全
一、引言:从生活中的单例说起
想象一下,我们每天上班打卡时使用的指纹识别机。整个公司只需要一台这样的设备,所有员工都通过它来记录考勤。如果每个部门都安装一台,不仅浪费资源,还会导致考勤数据不一致。这就是现实生活中的"单例"概念——确保某个类只有一个实例,并提供一个全局访问点。
在软件开发中,单例模式(Singleton Pattern)是一种创建型设计模式,它保证一个类只有一个实例,并提供一个访问该实例的全局节点。这种模式在需要控制资源访问、配置管理、日志记录等场景中非常有用。
以上图表展示了现实生活中单例概念的多个例子,以及它们与软件开发中单例模式的关联。
为什么需要单例模式?
在实际开发中,我们经常会遇到以下场景:
- 资源共享:如数据库连接池、线程池等
- 配置管理:应用程序的全局配置
- 日志记录:统一的日志输出
- 缓存系统:全局缓存访问
- 设备驱动:如打印机设备
单例模式三大特点:
- 一个类只有一个实例
- 自行创建这个实例
- 向整个系统提供这个实例
二、单例模式的基本实现
理解了单例模式的概念后,我们来看看如何用代码实现一个基本的单例。单例模式的核心在于:
- 私有化构造函数,防止外部直接实例化
- 提供一个静态方法获取唯一实例
- 考虑线程安全问题
1. 饿汉式单例
饿汉式是最简单的实现方式,它在类加载时就创建实例,保证了线程安全。
Java实现 - EagerSingleton.java
/**
* 饿汉式单例实现
* 优点:实现简单,线程安全
* 缺点:类加载时就初始化,可能造成资源浪费
*/
public class EagerSingleton {
// 类加载时就初始化,final保证不可变
private static final EagerSingleton instance = new EagerSingleton();
// 私有构造函数,防止外部实例化
private EagerSingleton() {
// 防止反射攻击
if (instance != null) {
throw new IllegalStateException("Singleton already initialized");
}
System.out.println("EagerSingleton instance created");
}
// 全局访问点
public static EagerSingleton getInstance() {
return instance;
}
// 示例方法
public void showMessage() {
System.out.println("Hello from EagerSingleton!");
}
// 防止反序列化破坏单例
protected Object readResolve() {
return getInstance();
}
}
实现细节分析:
- 静态初始化:instance变量在类加载时初始化,由JVM保证线程安全
- 私有构造器:防止外部通过new关键字创建实例
- final关键字:确保instance引用不可变
- 反射防护:在构造器中检查实例是否存在,防止反射攻击
- 反序列化防护:readResolve方法确保反序列化时返回同一实例
测试代码:
Java测试代码 - EagerSingletonTest.java
public class EagerSingletonTest {
public static void main(String[] args) {
// 获取单例实例
EagerSingleton singleton1 = EagerSingleton.getInstance();
EagerSingleton singleton2 = EagerSingleton.getInstance();
// 调用方法
singleton1.showMessage();
// 验证是否是同一实例
System.out.println("singleton1 == singleton2: " + (singleton1 == singleton2));
// 输出hashCode
System.out.println("singleton1 hashCode: " + singleton1.hashCode());
System.out.println("singleton2 hashCode: " + singleton2.hashCode());
// 尝试反射攻击
try {
Constructor<EagerSingleton> constructor = EagerSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
EagerSingleton illegalInstance = constructor.newInstance();
System.out.println("通过反射创建的实例: " + illegalInstance.hashCode());
} catch (Exception e) {
System.out.println("反射攻击失败: " + e.getMessage());
}
}
}
测试代码验证了单例的正确性,包括实例唯一性检查和反射攻击防护。
2. 懒汉式单例
懒汉式与饿汉式相反,它在第一次被调用时才创建实例,避免了资源浪费。
Java实现 - LazySingleton.java
/**
* 基础懒汉式单例实现
* 优点:延迟加载,节省资源
* 缺点:非线程安全
*/
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
System.out.println("LazySingleton instance created");
}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
public void showMessage() {
System.out.println("Hello from LazySingleton!");
}
}
线程安全问题演示:
Java测试代码 - LazySingletonThreadTest.java
public class LazySingletonThreadTest {
public static void main(String[] args) {
// 创建多个线程测试
for (int i = 0; i < 5; i++) {
new Thread(() -> {
LazySingleton singleton = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ": " + singleton.hashCode());
}).start();
}
}
}
运行此测试代码,可能会看到不同的hashCode输出,证明基础懒汉式实现在多线程环境下会创建多个实例。
以上序列图展示了基础懒汉式在多线程环境下可能创建多个实例的问题。
三、线程安全的单例实现
在实际应用中,我们经常需要考虑多线程环境下的安全性。让我们深入探讨几种线程安全的单例实现方式。
1. 同步方法实现
最简单的线程安全实现是在getInstance()方法上加synchronized关键字。
Java实现 - SynchronizedSingleton.java
/**
* 同步方法实现的线程安全单例
* 优点:实现简单,线程安全
* 缺点:每次调用都同步,性能较差
*/
public class SynchronizedSingleton {
private static SynchronizedSingleton instance;
private SynchronizedSingleton() {
System.out.println("SynchronizedSingleton instance created");
}
public static synchronized SynchronizedSingleton getInstance() {
if (instance == null) {
instance = new SynchronizedSingleton();
}
return instance;
}
public void showMessage() {
System.out.println("Hello from SynchronizedSingleton!");
}
}
性能测试:
Java性能测试代码 - SingletonPerformanceTest.java
public class SingletonPerformanceTest {
private static final int THREAD_COUNT = 1000;
private static final int LOOP_COUNT = 100000;
public static void main(String[] args) {
// 测试同步方法实现的性能
long start = System.currentTimeMillis();
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
executor.execute(() -> {
for (int j = 0; j < LOOP_COUNT; j++) {
SynchronizedSingleton.getInstance();
}
});
}
executor.shutdown();
while (!executor.isTerminated()) {
// 等待所有任务完成
}
long end = System.currentTimeMillis();
System.out.println("同步方法实现耗时: " + (end - start) + "ms");
}
}
这个测试展示了同步方法实现在高并发场景下的性能问题。
2. 双重检查锁定(DCL)
双重检查锁定是一种更高效的线程安全实现方式。
Java实现 - DoubleCheckedSingleton.java
/**
* 双重检查锁定实现的线程安全单例
* 优点:线程安全,性能较好
* 缺点:实现较复杂,需要注意volatile和指令重排序问题
*/
public class DoubleCheckedSingleton {
// volatile确保可见性和防止指令重排序
private volatile static DoubleCheckedSingleton instance;
private DoubleCheckedSingleton() {
System.out.println("DoubleCheckedSingleton instance created");
}
public static DoubleCheckedSingleton getInstance() {
// 第一次检查,不加锁
if (instance == null) {
synchronized (DoubleCheckedSingleton.class) {
// 第二次检查,加锁后
if (instance == null) {
instance = new DoubleCheckedSingleton();
/*
* 对象的创建分为三步:
* 1. 分配内存空间
* 2. 初始化对象
* 3. 将instance指向分配的内存地址
* 如果不加volatile,可能发生指令重排序导致2和3步骤交换
*/
}
}
}
return instance;
}
public void showMessage() {
System.out.println("Hello from DoubleCheckedSingleton!");
}
}
关键点解析:
- volatile关键字:确保多线程环境下的可见性,防止指令重排序
- 双重检查:第一次检查不加锁提高性能,第二次检查保证线程安全
- 同步块:只在实例未创建时同步,创建后不再同步
- 指令重排序问题:不加volatile可能导致其他线程获取到未完全初始化的实例
以上流程图详细展示了双重检查锁定的执行流程。
3. 静态内部类实现
静态内部类实现是一种更优雅的线程安全单例实现方式。
Java实现 - InnerClassSingleton.java
/**
* 静态内部类实现的线程安全单例
* 优点:线程安全,延迟加载,实现简洁
* 缺点:无法传递参数初始化
*/
public class InnerClassSingleton {
private InnerClassSingleton() {
System.out.println("InnerClassSingleton instance created");
}
// 静态内部类在第一次被引用时才会加载
private static class SingletonHolder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
public void showMessage() {
System.out.println("Hello from InnerClassSingleton!");
}
// 防止反序列化破坏单例
protected Object readResolve() {
return getInstance();
}
}
最佳实践:
静态内部类实现是大多数场景下的最佳选择,因为:
- 由JVM保证线程安全
- 实现简洁明了
- 延迟加载,不浪费资源
- 不需要同步,性能好
只有在需要传递初始化参数时才考虑使用双重检查锁定。
四、枚举实现与高级话题
枚举实现是《Effective Java》作者Joshua Bloch推荐的单例实现方式,它不仅能避免多线程同步问题,还能防止反序列化重新创建新的对象。
Java实现 - EnumSingleton.java
/**
* 枚举实现的单例模式
* 优点:线程安全,防止反射攻击,自动处理序列化
* 缺点:无法延迟加载
*/
public enum EnumSingleton {
INSTANCE;
// 可以添加任意方法和属性
private String data;
public void setData(String data) {
this.data = data;
}
public String getData() {
return data;
}
public void showMessage() {
System.out.println("Hello from EnumSingleton! Data: " + data);
}
}
枚举单例的优势:
- 线程安全:枚举实例的创建由JVM保证
- 防止反射攻击:Java规范禁止反射创建枚举实例
- 自动序列化机制:枚举的序列化由JVM特殊处理,保证反序列化时不会创建新实例
- 代码简洁:实现非常简单
Java测试代码 - EnumSingletonTest.java
public class EnumSingletonTest {
public static void main(String[] args) throws Exception {
// 正常使用
EnumSingleton singleton1 = EnumSingleton.INSTANCE;
singleton1.setData("Test Data");
singleton1.showMessage();
// 验证单例
EnumSingleton singleton2 = EnumSingleton.INSTANCE;
System.out.println("singleton1 == singleton2: " + (singleton1 == singleton2));
// 尝试反射攻击
try {
Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
EnumSingleton illegalInstance = constructor.newInstance();
System.out.println("通过反射创建的实例: " + illegalInstance);
} catch (Exception e) {
System.out.println("反射攻击失败: " + e.getMessage());
}
// 测试序列化
try {
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(singleton1);
oos.close();
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
EnumSingleton deserialized = (EnumSingleton) ois.readObject();
System.out.println("原始实例: " + singleton1.hashCode());
System.out.println("反序列化实例: " + deserialized.hashCode());
System.out.println("singleton1 == deserialized: " + (singleton1 == deserialized));
} catch (Exception e) {
e.printStackTrace();
}
}
}
高级话题:单例模式在分布式系统中的挑战
在分布式系统中,传统的单例模式会遇到挑战,因为每个JVM都会有自己的单例实例。要真正实现全局单例,可以考虑:
- 使用分布式缓存:如Redis实现全局单例
- 数据库唯一约束:通过数据库保证唯一性
- 分布式锁:如Zookeeper实现分布式锁
Redis实现的分布式单例示例
public class DistributedSingleton {
private static final String REDIS_KEY = "distributed_singleton";
private static final int EXPIRE_SECONDS = 60;
private final JedisPool jedisPool;
private final String data;
private DistributedSingleton(JedisPool jedisPool, String data) {
this.jedisPool = jedisPool;
this.data = data;
}
public static synchronized DistributedSingleton getInstance(JedisPool jedisPool, String data) {
try (Jedis jedis = jedisPool.getResource()) {
// 尝试设置Redis键,如果成功表示获取单例权限
String result = jedis.set(REDIS_KEY, data, "NX", "EX", EXPIRE_SECONDS);
if ("OK".equals(result)) {
return new DistributedSingleton(jedisPool, data);
}
// 如果键已存在,获取当前值
String currentData = jedis.get(REDIS_KEY);
return new DistributedSingleton(jedisPool, currentData);
}
}
public String getData() {
return data;
}
public void refresh() {
try (Jedis jedis = jedisPool.getResource()) {
jedis.expire(REDIS_KEY, EXPIRE_SECONDS);
}
}
}
五、单例模式的实战应用
让我们通过几个实际案例来看看单例模式在真实项目中的应用。
1. 配置管理单例
Java实现 - AppConfig.java
/**
* 应用程序配置管理单例
*/
public class AppConfig {
private static final AppConfig INSTANCE = new AppConfig();
private Properties properties;
private AppConfig() {
loadConfig();
}
public static AppConfig getInstance() {
return INSTANCE;
}
private void loadConfig() {
properties = new Properties();
try (InputStream input = getClass().getClassLoader().getResourceAsStream("config.properties")) {
if (input == null) {
throw new RuntimeException("Unable to find config.properties");
}
properties.load(input);
} catch (IOException e) {
throw new RuntimeException("Error loading configuration", e);
}
}
public String getProperty(String key) {
return properties.getProperty(key);
}
public String getProperty(String key, String defaultValue) {
return properties.getProperty(key, defaultValue);
}
// 其他配置相关方法...
}
2. 日志记录单例
Java实现 - Logger.java
/**
* 日志记录单例
*/
public class Logger {
private static volatile Logger instance;
private PrintWriter logWriter;
private Logger() {
try {
// 初始化日志文件
logWriter = new PrintWriter(new FileWriter("application.log", true), true);
} catch (IOException e) {
throw new RuntimeException("Failed to initialize logger", e);
}
}
public static Logger getInstance() {
if (instance == null) {
synchronized (Logger.class) {
if (instance == null) {
instance = new Logger();
}
}
}
return instance;
}
public void log(String message) {
String logEntry = String.format("[%s] %s",
LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
message);
logWriter.println(logEntry);
}
public void close() {
if (logWriter != null) {
logWriter.close();
}
}
}
3. 数据库连接池单例
Java实现 - ConnectionPool.java
/**
* 数据库连接池单例
*/
public class ConnectionPool {
private static final int MAX_POOL_SIZE = 10;
private static final ConnectionPool INSTANCE = new ConnectionPool();
private final BlockingQueue<Connection> pool;
private final String url;
private final String user;
private final String password;
private ConnectionPool() {
// 从配置加载数据库连接信息
AppConfig config = AppConfig.getInstance();
this.url = config.getProperty("db.url");
this.user = config.getProperty("db.user");
this.password = config.getProperty("db.password");
this.pool = new LinkedBlockingQueue<>(MAX_POOL_SIZE);
initializePool();
}
public static ConnectionPool getInstance() {
return INSTANCE;
}
private void initializePool() {
try {
for (int i = 0; i < MAX_POOL_SIZE; i++) {
Connection conn = DriverManager.getConnection(url, user, password);
pool.offer(conn);
}
} catch (SQLException e) {
throw new RuntimeException("Failed to initialize connection pool", e);
}
}
public Connection getConnection() throws InterruptedException {
return pool.take();
}
public void releaseConnection(Connection conn) {
if (conn != null) {
pool.offer(conn);
}
}
public void closeAll() {
for (Connection conn : pool) {
try {
if (!conn.isClosed()) {
conn.close();
}
} catch (SQLException e) {
// 记录错误但继续关闭其他连接
Logger.getInstance().log("Error closing connection: " + e.getMessage());
}
}
}
}
以上类图展示了三个单例类之间的关系及其主要方法。
六、总结与最佳实践
通过本文的全面探讨,我们对单例模式有了深入的理解。让我们总结一下各种实现方式的对比和最佳实践。
实现方式 | 线程安全 | 延迟加载 | 防止反射 | 防止序列化 | 性能 | 复杂度 | 适用场景 |
---|---|---|---|---|---|---|---|
饿汉式 | 是 | 否 | 否 | 需额外处理 | 高 | 低 | 初始化简单,不介意提前加载 |
懒汉式(同步) | 是 | 是 | 否 | 需额外处理 | 低 | 中 | 不推荐使用 |
双重检查锁定 | 是 | 是 | 否 | 需额外处理 | 高 | 高 | 需要延迟加载且需要传递参数 |
静态内部类 | 是 | 是 | 否 | 需额外处理 | 高 | 中 | 大多数场景的最佳选择 |
枚举 | 是 | 否 | 是 | 是 | 高 | 低 | 不需要延迟加载的场景 |
单例模式最佳实践:
- 优先选择枚举或静态内部类实现:它们简洁、安全且高效
- 考虑初始化性能:如果初始化耗时,考虑使用延迟加载
- 注意资源释放:单例生命周期与应用程序相同,确保资源正确释放
- 避免滥用:不要将所有全局状态都设计为单例,考虑依赖注入
- 测试考虑:单例可能使单元测试复杂化,考虑使用依赖注入替代
- 分布式环境:在分布式系统中,需要特殊处理才能实现真正的单例
希望通过这篇全面的单例模式指南,能帮助大家在项目中做出更合理的设计决策。记住,设计模式是工具而不是目标,应该根据实际需求灵活运用。
如果你有任何问题或想分享自己的单例模式使用经验,欢迎随时交流讨论。让我们共同进步,写出更优雅、更健壮的代码!