单例模式完整教程
设计模式中最基础也最常用的创建型模式,保证一个类在整个应用中只有一个实例
一、模式详细介绍
基本定义
单例模式(Singleton Pattern)是一种创建型设计模式,它保证一个类在整个应用程序的生命周期中 只能创建一个实例,并且提供一个全局统一的访问点来获取这个实例。
核心思想
通过私有化构造方法,阻止外部直接实例化类;然后通过一个静态方法来控制类的实例化过程, 确保无论调用多少次这个方法,都只会返回同一个对象实例。
核心特点
✅ 唯一性
一个类在整个应用中只能有一个实例
✅ 全局访问性
提供一个全局统一的访问点
✅ 延迟初始化
可以在第一次使用时才创建实例
✅ 线程安全
多线程环境下也能保证唯一性
二、单例模式的核心优势
1 精确控制资源访问
对于需要独占访问的资源(如数据库连接池、文件系统、打印机等),单例模式可以确保只有一个实例在操作这些资源, 避免了多个实例同时访问资源导致的冲突、数据不一致和资源竞争问题。
2 显著节省系统资源
创建和销毁对象是有性能开销的,尤其是对于那些创建成本高的对象(如数据库连接、网络连接、配置管理器等)。 单例模式避免了频繁创建和销毁对象的开销,只创建一次实例并重复使用,可以显著提高系统性能。
3 全局统一的状态管理
当需要在应用的多个部分之间共享状态时,单例模式提供了一个全局统一的状态存储和访问点。 所有对状态的修改和读取都通过同一个实例进行,保证了状态的一致性和可预测性。
4 简化代码结构
单例模式将对象的创建和使用分离,客户端不需要关心对象的创建细节,只需要通过统一的接口获取实例即可。 这简化了客户端代码,减少了重复的对象创建逻辑,提高了代码的可维护性。
5 灵活的生命周期控制
单例模式允许开发者精确控制实例的创建时机和生命周期。可以实现延迟加载(懒加载), 只有在真正需要时才创建实例,避免了不必要的资源占用;也可以实现饿汉式加载,在应用启动时就完成初始化。
实际应用场景:数据库连接池、日志记录器、配置管理器、应用上下文、线程池、缓存管理器、打印机管理器等。
三、示例说明
我们以一个数据库连接管理器为例来演示单例模式的实际应用。
在一个典型的Web应用中,数据库连接是一种非常宝贵的资源。如果每次数据库操作都创建一个新的连接池, 不仅会造成巨大的性能开销,还可能导致数据库连接数耗尽,引发系统崩溃。
使用单例模式,我们可以确保整个应用程序中只有一个DatabaseManager实例,
所有对数据库的操作都通过这个唯一的实例进行。这个实例负责管理数据库连接池,
统一分配和回收连接,从而实现资源的高效利用。
这样不仅可以节省系统资源,还可以统一管理数据库连接的生命周期,避免连接泄漏, 提高系统的稳定性和可维护性。
四、UML类图
注:__init__方法为私有方法,防止外部直接实例化;_lock用于保证线程安全
五、Python核心实现方式
单例模式最经典的两种实现方式是饿汉式和懒汉式,它们的主要区别在于实例创建的时机不同。
5.1 饿汉式(Eager Initialization)
最简单核心特点:在类加载时就立即创建实例,不管后续是否会使用。
优缺点:✅ 线程安全,实现简单;❌ 无法实现延迟加载,如果实例从未被使用会造成资源浪费。
class DatabaseManager:
# 类加载时就创建唯一实例
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, connection_string):
# 初始化数据库连接池(只会执行一次)
if not hasattr(self, 'initialized'):
self.connection_string = connection_string
self.pool = self._create_connection_pool()
print(f"数据库连接池初始化完成: {connection_string}")
self.initialized = True
def _create_connection_pool(self):
# 创建数据库连接池的实际逻辑
return {"connections": 10, "max_connections": 20}
def get_connection(self):
# 从连接池获取连接
return "数据库连接对象"
# 测试
if __name__ == "__main__":
# 无论调用多少次,都只会创建一个实例
db1 = DatabaseManager("mysql://localhost:3306/test")
db2 = DatabaseManager("mysql://localhost:3306/prod")
print(db1 is db2) # 输出: True
print(db1.connection_string) # 输出: mysql://localhost:3306/test
print(db2.connection_string) # 输出: mysql://localhost:3306/test
注意:饿汉式的__init__方法仍然会在每次实例化时被调用,
因此需要添加initialized标记来防止重复初始化。
5.2 基础懒汉式(Lazy Initialization)
延迟加载核心特点:在第一次调用获取实例的方法时才创建实例,实现了延迟加载。
优缺点:✅ 节省资源,只有在需要时才创建实例;❌ 非线程安全,多线程环境下可能会创建多个实例。
class Logger:
_instance = None
def __init__(self, log_file="app.log"):
self.log_file = log_file
print(f"日志记录器初始化完成,日志文件: {log_file}")
@classmethod
def get_instance(cls, log_file="app.log"):
# 只有在第一次调用时才创建实例
if not cls._instance:
cls._instance = cls(log_file)
return cls._instance
def log(self, message):
print(f"[{self.log_file}] {message}")
# 测试
if __name__ == "__main__":
logger1 = Logger.get_instance("app.log")
logger2 = Logger.get_instance("error.log")
print(logger1 is logger2) # 输出: True
logger1.log("这是一条日志") # 输出: [app.log] 这是一条日志
logger2.log("这是另一条日志") # 输出: [app.log] 这是另一条日志
警告:基础懒汉式在多线程环境下是不安全的。如果多个线程同时通过if not cls._instance判断,
就会创建多个实例,违反单例模式的初衷。
5.3 线程安全懒汉式(双重检查锁定)
⭐ 生产环境推荐核心特点:在基础懒汉式的基础上增加了双重检查锁定(Double-Checked Locking)机制, 既保留了延迟加载的优点,又保证了线程安全。
优缺点:✅ 线程安全,延迟加载,性能好;❌ 实现相对复杂一点。
import threading
class ConfigManager:
_instance = None
_lock = threading.Lock() # 线程锁
def __init__(self):
# 加载配置文件(只会执行一次)
self.config = self._load_config()
print("配置文件加载完成")
def _load_config(self):
# 实际的配置加载逻辑
return {"debug": True, "port": 8000, "database": "mysql://localhost:3306/test"}
@classmethod
def get_instance(cls):
# 第一次检查:如果实例已存在,直接返回,避免不必要的锁竞争
if not cls._instance:
# 获取锁,保证只有一个线程进入
with cls._lock:
# 第二次检查:防止多个线程同时通过第一次检查
if not cls._instance:
cls._instance = cls()
return cls._instance
def get(self, key, default=None):
return self.config.get(key, default)
# 多线程测试
def test_singleton():
config = ConfigManager.get_instance()
print(f"线程 {threading.get_ident()}: {id(config)}")
if __name__ == "__main__":
# 创建10个线程同时获取实例
threads = []
for _ in range(10):
t = threading.Thread(target=test_singleton)
threads.append(t)
t.start()
for t in threads:
t.join()
# 所有线程都会得到同一个实例
config1 = ConfigManager.get_instance()
config2 = ConfigManager.get_instance()
print(f"config1 is config2: {config1 is config2}") # 输出: True
六、对比总结
| 实现方式 | 线程安全 | 延迟加载 | 代码复杂度 | 适用场景 |
|---|---|---|---|---|
| 饿汉式 | ✅ 是 | ❌ 否 | 低 | 实例一定会被使用、创建成本低 |
| 基础懒汉式 | ❌ 否 | ✅ 是 | 低 | 单线程环境、简单测试场景 |
| 线程安全懒汉式 | ✅ 是 | ✅ 是 | 中 | 生产环境、多线程应用 |
✅ 单例模式优点
- 保证一个类只有一个实例,节省系统资源
- 提供全局统一的访问点,方便管理
- 可以精确控制实例的创建时机和生命周期
- 避免了频繁创建和销毁对象的性能开销
- 保证了全局状态的一致性
❌ 单例模式缺点
- 违反单一职责原则,同时负责创建和业务逻辑
- 引入全局状态,增加了代码的耦合度
- 不利于单元测试,状态会在测试之间共享
- 扩展困难,无法通过继承来扩展功能
- 在多线程环境下需要额外处理线程安全
七、使用建议
- 生产环境优先使用线程安全懒汉式: 双重检查锁定的实现方式在性能、安全性和资源利用率之间取得了最好的平衡,是绝大多数生产环境下的首选。
- 简单场景可以使用饿汉式: 如果确定实例一定会被使用,且创建成本不高,饿汉式是最简单可靠的实现方式。
- 避免使用基础懒汉式: 基础懒汉式在多线程环境下存在严重的线程安全问题,除非是完全的单线程环境,否则不要使用。
- 避免滥用单例模式: 单例模式会引入全局状态,增加代码的耦合度和测试难度。只有在确实需要全局唯一实例时才使用,不要为了方便而滥用。
- 注意单例的生命周期: 单例实例一旦创建,会一直存在于内存中,直到应用程序退出。对于占用大量内存的单例,要注意及时释放不必要的资源。