Python 单例模式:从 new 到线程安全实现
在面向对象编程中,单例模式是一个非常经典的设计模式。
它要解决的问题很简单:
一个类在整个程序运行过程中,只允许创建一个实例。
比如数据库连接池、配置对象、日志对象、全局缓存管理器,这些对象通常不需要反复创建多个实例。这个时候,单例模式就很有用。
本文会结合 Python 中的 __new__ 方法,讲清楚单例模式的实现方式,并进一步说明为什么在多线程环境下还需要加锁。
1. 先理解 new 方法
在 Python 中,创建对象时通常会调用类:
python
class Foo:
pass
foo = Foo()
很多人熟悉 __init__(),因为它负责初始化对象。
但在 __init__() 之前,其实还有一个更底层的方法:__new__()。
可以简单理解为:
__new__()负责创建对象;__init__()负责初始化对象。
示例:
python
class Foo:
def __new__(cls, *args, **kwargs):
return super().__new__(cls)
当执行:
python
foo1 = Foo()
foo2 = Foo()
每调用一次 Foo(),都会创建一个新的对象。
所以正常情况下:
python
print(foo1 == foo2)
结果通常是:
text
False
因为 foo1 和 foo2 是两个不同的实例。
2. 什么是单例模式
单例模式的目标是:
无论创建多少次对象,最终拿到的都是同一个实例。
也就是说:
python
s1 = Singleton()
s2 = Singleton()
我们希望:
python
s1 == s2
结果为:
text
True
更准确地说,它们不仅值相等,而且应该是同一个对象。
可以使用 is 判断对象身份:
python
print(s1 is s2)
如果输出 True,说明两个变量指向的是同一个实例。
3. 使用 new 实现单例
Python 中实现单例的一种常见方式,就是重写 __new__()。
示例代码如下:
python
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
这段代码的关键在于类变量:
python
_instance = None
它用于保存唯一的实例。
创建对象时,会先进入 __new__():
python
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
逻辑是:
- 第一次调用
Singleton()时,_instance是None; - 程序创建一个新对象,并保存到
_instance; - 第二次再调用
Singleton()时,_instance已经有值; - 直接返回之前创建好的对象。
测试一下:
python
s1 = Singleton()
s2 = Singleton()
print(s1 is s2)
输出:
text
True
这样,一个最基础的单例模式就实现了。
4. 单线程下没问题,多线程下可能出问题
上面的写法在单线程环境中通常没问题。
但如果放到多线程环境里,就可能出现线程安全问题。
问题出在这段判断和创建逻辑:
python
if cls._instance is None:
cls._instance = super().__new__(cls)
它看起来只有两行,但并不是一个不可分割的原子操作。
假设多个线程同时执行到这里:
text
线程 A 判断 _instance 是 None
线程 B 也判断 _instance 是 None
线程 A 创建对象
线程 B 也创建对象
这样就可能出现多个实例。
这和单例模式的目标是冲突的。
所以,在多线程环境下实现单例,需要加入锁。
5. 使用 RLock 实现线程安全单例
可以使用 threading.RLock() 给创建实例的过程加锁。
代码如下:
python
import threading
class Singleton:
_instance = None
lock = threading.RLock()
def __new__(cls):
with cls.lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
这里新增了一个类变量:
python
lock = threading.RLock()
然后在 __new__() 中使用:
python
with cls.lock:
它的作用是:
- 一个线程进入代码块时,会先获得锁;
- 在这个线程释放锁之前,其他线程不能进入这段关键代码;
- 这样就能保证同一时间只有一个线程执行实例创建逻辑。
因此,即使有多个线程同时创建 Singleton 对象,也只能创建出一个实例。
6. 多线程测试
可以写一个任务函数,在线程中创建单例对象:
python
def task():
obj = Singleton()
print(obj)
然后启动多个线程:
python
for i in range(10):
t = threading.Thread(target=task)
t.start()
如果单例生效,打印出来的对象地址应该是一样的。
例如输出可能类似:
text
<__main__.Singleton object at 0x000001F4E2A4A4D0>
<__main__.Singleton object at 0x000001F4E2A4A4D0>
<__main__.Singleton object at 0x000001F4E2A4A4D0>
虽然打印了很多次,但对象地址相同,说明它们都是同一个实例。
7. 为什么使用 with 管理锁
锁也可以手动获取和释放:
python
cls.lock.acquire()
try:
...
finally:
cls.lock.release()
但更推荐使用 with:
python
with cls.lock:
...
原因是 with 会自动帮我们释放锁。
即使代码块中出现异常,也不容易因为忘记释放锁而导致死锁。
所以在 Python 并发代码中,管理锁时优先使用 with 是一个好习惯。
8. Lock 和 RLock 的简单区别
代码中使用的是 threading.RLock()。
RLock 叫可重入锁。
它和普通的 Lock 有一个重要区别:
Lock:同一个线程不能重复获取同一把锁;RLock:同一个线程可以重复获取同一把锁,但需要释放相同次数。
在这个简单单例示例中,使用 Lock 通常也可以。
但如果后续代码中出现嵌套调用,并且同一个线程可能重复进入同一把锁保护的区域,RLock 会更灵活。
9. 这种单例写法的注意点
虽然通过 __new__() 实现单例很直观,但实际开发时还需要注意几个问题。
第一,__init__() 可能会被多次调用。
即使 __new__() 返回的是同一个对象,每次调用 Singleton() 时,__init__() 仍然可能执行。
如果类中有初始化逻辑,需要避免重复初始化。
可以加一个标记:
python
class Singleton:
_instance = None
_initialized = False
lock = threading.RLock()
def __new__(cls):
with cls.lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if self._initialized:
return
self.config = {}
self._initialized = True
第二,单例会引入全局状态。
单例对象在程序中到处都可以被拿到,如果内部状态经常被修改,可能会让代码变得难测试、难维护。
所以不要因为"设计模式"听起来高级,就到处使用单例。
它适合真正需要全局唯一实例的场景。
10. 单例模式适合哪些场景
单例模式适合用于:
- 全局配置管理器;
- 日志记录器;
- 数据库连接池;
- 线程池管理器;
- 缓存对象;
- 应用运行时上下文。
这些对象通常有一个共同特点:
多个地方都需要访问,但整个程序中只需要一个实例。
如果一个类本身没有共享状态,或者创建多个实例也不会造成问题,就不一定需要使用单例。
11. 小结
本文从 __new__() 方法开始,讲解了 Python 中单例模式的基本实现,并进一步说明了多线程下为什么需要加锁。
核心结论如下:
__new__()负责创建对象,__init__()负责初始化对象;- 单例模式的目标是让一个类只创建一个实例;
- 可以通过类变量
_instance保存唯一实例; - 重写
__new__()可以控制对象创建过程; - 多线程环境下,判断和创建实例的过程可能发生竞争;
- 使用
threading.RLock()可以保证创建实例的过程线程安全; - 使用
with lock:管理锁更安全; - 单例适合全局唯一资源,但不要滥用。
如果用一句话总结:
Python 单例模式的关键,就是把对象创建权收回到
__new__()中,并在多线程环境下用锁保护创建过程。