因为__new__不是原子操作:线程A判断_instance为None后,未执行super().__new__前,线程B也判为None,导致重复创建;GIL不保护跨语句逻辑,需用双重检查锁定(DCL)加threading.Lock确保线程安全。

为什么直接用 __new__ 实现单例在多线程下会失效
因为 __new__ 本身不是原子操作:线程 A 判断 _instance 为 None 后,还没来得及执行 super().__new__,线程 B 就进来了,也判断为 None,结果两个线程各自创建实例并赋值,单例被破坏。
常见错误现象是:日志里看到两次初始化逻辑(比如 __init__ 被执行两次),或数据库连接对象出现多个副本。
- 即使加了
if cls._instance is None:,也不构成临界区保护 -
__new__不自动同步,Python 解释器不会帮你加锁 - CPython 的 GIL 不能保证这个场景安全——GIL 只保某些字节码原子,不保跨语句逻辑
用 threading.Lock 实现线程安全单例(推荐初学者)
核心思路:把整个实例创建逻辑包进 with lock: 块,确保同一时刻只有一个线程能进入构造路径。
示例中注意 _lock 必须是类变量且提前初始化,否则锁对象自己就可能被并发创建:
import threading
<p>class Singleton:
_instance = None
_lock = threading.Lock() # 类变量,必须在类定义内初始化</p><pre class="brush:php;toolbar:false;">def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None: # 双重检查锁定(DCL)
cls._instance = super().__new__(cls)
return cls._instance- 必须用双重检查(DCL):外层避免每次调用都抢锁,内层防止锁释放后重复创建
-
_lock不能放在__new__里初始化,否则多个线程可能各自建一个锁,形同虚设 - 如果
__init__有耗时操作(如读配置、连 DB),建议把它也放进with cls._lock:块里,否则仍可能被并发执行
用元类 metaclass 实现可复用的线程安全单例
当你需要多个类都支持线程安全单例时,元类比每个类手写 __new__ 更干净。关键是把锁和实例缓存提到元类层级,而非每个类自己维护。
注意元类的 __call__ 是类被调用(即 MyClass())时触发的入口,这里才是真正的“构造控制点”:
import threading
<p>class ThreadSafeSingletonMeta(type):
_instances = {}
_locks = {}</p><pre class="brush:php;toolbar:false;">def __call__(cls, *args, **kwargs):
if cls not in cls._locks:
cls._locks[cls] = threading.Lock()
with cls._locks[cls]:
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]class Config(metaclass=ThreadSafeSingletonMeta): def init(self): self.data = {"host": "localhost"}
-
_locks和_instances都是元类的类变量,按cls键隔离,避免不同单例类互相干扰 - 不要在元类
__new__或__init__里做实例管理——它们只在类定义时运行一次,管不了实例化过程 - 如果子类未显式指定
metaclass,会继承父类元类;但若父类是普通类,子类需显式声明,否则元类不生效
Lock 和 metaclass 方案的实际差异与取舍
两者都能解决核心问题,但适用场景不同:Lock 方案轻量、易调试、适合单个关键类;metaclass 方案抽象度高,适合框架层统一管控,但也引入了额外复杂度。
- 性能上:无竞争时几乎无差别;高并发争抢时,
threading.Lock开销远小于元类查表+字典操作,但通常这不是瓶颈 - 兼容性:Lock 方案在任何 Python 版本都稳定;metaclass 在 PyPy 或某些静态分析工具中可能触发警告
- 最容易被忽略的一点:如果你的单例类需要支持
__init__参数(比如Singleton(url="...")),两种方案都要确保参数只在首次创建时生效,后续调用应忽略参数或抛出异常——否则语义就不是“单例”了
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











