介绍
✅ 一、什么是单例模式?
一个类在整个程序运行过程中,只能被创建一次实例,且这个实例是全局共享的。
✅ 二、为什么要用单例模式?
适用于那些:
• 系统中只应该有一个实例存在的对象
• 该对象需要被多个地方共享使用(比如:配置类、数据库连接池、线程池、缓存、日志器)
相关模式实现
⭐ 方式一:最经典的懒汉式(线程不安全)
只有在第一次调用时才创建实例,之前不初始化。
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
🧠 所以它叫“懒汉”:
• 懒:我不主动初始化
• 只有你来用我时,我才出手!
• 第一次调用时创建实例
• 之后返回同一个实例
• 问题:多线程下可能出现创建多个实例
🧠 表面看上去是“只初始化一次”,但其实是 线程不安全的
在多线程并发环境下,有多个线程
几乎同时进入 new,几乎同时判断到 _instance is None,然后都去 new 一个对象
• 线程A:判断 _instance is None ✅ → 正在执行 super().__new__(cls) 还没赋值;
• 同时线程B 也进来了,判断 _instance is None ✅ → 也执行 super().__new__(cls);
• 最终:两个对象都被创建,并都赋值给 _instance → 后来者覆盖前者;
线程A: new Singleton() -> instance_A
线程B: new Singleton() -> instance_B
最终 cls._instance = instance_B
虽然最后 cls._instance 只保留一个,但
已经有两个不同的对象被初始化过了!
与之相对的是饿汉模式:
程序一启动,就主动把实例创建好,无论你用不用,我都提前准备好了!
class Singleton:
...
single_ton = Singleton()
优点:实现最简单,天然线程安全 ✅
缺点:不够懒惰,可能浪费资源 ❌(比如 RedisClient 在启动时并未使用)
模式 | 创建时机 | 优点 | 缺点 |
---|---|---|---|
懒汉式 | 第一次使用时 | 节省资源 | 线程不安全(需加锁) |
饿汉式 | 模块加载时就创建 | 实现简单、线程安全 | 无需立即用也会占资源 |
⭐ 方式二:线程安全(加锁)
import threading
class Singleton:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
双重检查锁(Double-Checked Locking)
保证线程安全 ✅
第一个 if 是快速通道:大多数情况下跳过锁,提高性能;
第二个 if 是防止两个线程都卡在锁前,进去后又创建两次;
with cls._lock 保证多线程下只创建一次实例!
⭐ 方式三:使用装饰器封装
def singleton(cls): # 接收类作为参数
instances = {} # 用于缓存创建过的实例
def wrapper(*args, **kwargs): # 实际替代原来的类构造器
if cls not in instances: # 如果没有创建过,就创建一次
instances[cls] = cls(*args, **kwargs)
return instances[cls] # 返回这个唯一实例
return wrapper # 返回包装后的类构造函数
@singleton
class MyClass:
pass
>>>>> 相当于:
MyClass = singleton(MyClass)
a = MyClass()
b = MyClass()
print(a is b) # True ✅
这段代码的目的是让某个类 只能被实例化一次 —— 后续所有实例化操作都会返回第一次创建的对象。
⚠️ 注意事项
无法重设参数:如果第一次传参错误了,后续也只能拿到那个错误参数的实例;
线程不安全:这个 instances 字典不是线程安全的,如果在高并发下要加锁(可以用 threading.Lock());
作用范围是进程级别:不是跨进程或分布式单例,如果你用 Gunicorn 等部署,每个 worker 都是独立的。
@singleton 装饰器是一种函数式实现单例模式,优雅、简单、可复用,适合绝大多数轻量场景。
⭐ 方式四:使用模块本身(Pythonic 最推荐)
# config.py
value = {} # 定义一个全局变量
# main.py
import config
config.value['a'] = 123
🌟 Python 的模块,其实就是天然的“单例”对象!
🧠 Python 模块加载机制:
Python 会先去 sys.modules 查这个模块是否已经加载过。
如果没加载,就会执行一次 config.py,并把执行结果缓存下来;
如果以后其他地方再 import config,不会重新执行,而是直接使用缓存的模块对象!
这就达到了“全局唯一、全局共享状态
点 | 说明 |
---|---|
模块只初始化一次 | Python 的 import 是惰性且缓存的 |
所有地方 import 的是同一个对象 | 模块对象在 sys.modules 中唯一 |
修改模块内部变量,所有引用方可见 | 所以可实现“全局共享状态” |
非常适合做配置、状态管理、缓存等 | 比如 settings.py, global_store.py 等 |
✅ 示例升级:模拟配置中心
# config.py
settings = {
"env": "dev",
"db": {}
}
# service.py
import config
config.settings["db"]["host"] = "127.0.0.1"
# handler.py
import config
print(config.settings["db"]["host"]) # 127.0.0.1 ✅
实现方式 | 优势 | 典型用途 |
---|---|---|
模块作为单例 | 简单优雅,天然支持 | 配置、全局变量、缓存对象等 |
类实现单例 | 更复杂,适合封装行为 | RedisClient、Logger、连接池等 |
线程锁
线程锁是一种同步机制,用来确保多个线程访问共享资源时,不会发生冲突或竞争
📌 简单理解:
就像一个“🔒厕所门锁”:
• 一次只能让一个人(线程)进去;
• 别人只能等着锁释放后才能进去;
• 避免两个线程同时进来把厕所搞炸了 🚽💥
在 Python 多线程中,多个线程是同时执行的(虽然有 GIL,但 I/O 场景或内部切换是并发的)。
import threading
lock = threading.Lock()
def safe_task():
with lock:
# 这里是线程安全的代码块
# 同一时刻只有一个线程能执行
global counter
counter += 1
>>>>>>>>>
lock.acquire()
try:
# 临界区
counter += 1
finally:
lock.release()
2️⃣ 控制共享资源访问(如:写文件、更新数据库、内存计数器)
方法 | 含义 |
---|---|
lock.acquire() | 请求锁,如果被其他线程持有,则阻塞直到获得 |
lock.release() | 释放锁,让其他线程可以进入 |
with lock: | 上下文写法,自动 acquire 和 release |
注意:
点 | 说明 |
---|---|
死锁 | 如果获取了锁却忘记释放,就会造成别的线程永远卡住 |
加锁粒度 | 不要加得太宽,否则会导致程序性能下降(串行化) |
多线程操作对象 | 加锁保护的对象必须是共享变量,避免没必要的加锁 |
threading.Lock() 是 Python 中用来控制多线程“访问共享资源”的原始同步工具,相当于设置一个“互斥区”,同一时间只允许一个线程进去执行关键逻辑。
场景
场景 | 描述 |
---|---|
日志系统 | 所有模块写日志用同一个 Logger |
数据库连接池 | 只初始化一次,避免频繁连接 |
配置管理类 | 读取一次配置,全局共享 |
缓存客户端(如 RedisClient) | 保持单个连接池对象 |
• 在多线程/多进程下使用,必须注意线程安全;
• 单例生命周期长,避免持有过多状态(会引起“脏数据”);
• 使用不当可能导致代码耦合度高,不利于测试(尤其是自动化测试中会污染状态)。