直接用__new__实现的单例在多线程下失效,是因为多个线程可能同时判断_instance为None并进入创建逻辑,导致生成多个实例;必须用锁保证“检查+创建”原子化,否则必然出现竞态。

为什么直接用 __new__ 实现的单例在多线程下会失效
因为多个线程可能同时判断 _instance 为 None,然后都进入创建逻辑,最终生成多个实例。这不是概率低的问题,而是确定性竞争——只要没加锁,就一定会在高并发时出错。
常见错误现象:Singleton() is Singleton() 返回 False;日志里看到构造函数被调用了多次;数据库连接池或配置管理器意外出现状态不一致。
关键点在于:必须保证「检查 + 创建」这两个动作原子化。不能只锁创建,也不能只锁返回。
用 threading.Lock 保护 __new__ 的完整初始化流程
最稳妥的做法是在 __new__ 开头就获取锁,并覆盖整个实例检查和创建过程。注意锁对象本身不能依赖类属性(否则首次访问又可能触发竞态),应定义为模块级变量或使用 threading.Lock() 直接初始化。
- 锁必须在
if cls._instance is None:判断之前就 acquire,否则判空和 new 之间仍有窗口 - 不要在
__init__里做单例逻辑——它可能被多次调用(比如子类重写后) - 如果类支持继承,需确保子类也走同一把锁,推荐将锁放在基类外或用
cls.__name__分锁(但通常单例不鼓励继承)
import threading <p>_singleton_lock = threading.Lock() _instance = None</p><p>class Singleton: def <strong>new</strong>(cls): global _instance with _singleton_lock: if _instance is None: _instance = super().<strong>new</strong>(cls) return _instance </p>
用 functools.lru_cache 替代手写锁(Python 3.2+)
如果你的单例不需要延迟初始化、不依赖运行时参数、且构造函数无副作用,lru_cache(maxsize=1) 是更轻量的选择。它底层用 C 实现的线程安全缓存,比 Python 层锁更快,也更难出错。
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
适用场景:配置加载器、工具类、无状态服务对象。不适用场景:需要传参初始化、构造过程含 I/O 或外部依赖、要求显式控制生命周期。
-
@lru_cache(maxsize=1)必须装饰一个函数,所以要把构造逻辑包装成工厂函数 - 函数不能有不可哈希参数(如
dict、list),否则缓存失效或报TypeError - 第一次调用该函数才真正执行构造,后续调用直接返回缓存对象
from functools import lru_cache <p>class ConfigLoader: def <strong>init</strong>(self):</p><h1>模拟耗时加载</h1><pre class="brush:php;toolbar:false;"> pass
@lru_cache(maxsize=1) def get_config_loader(): return ConfigLoader()
为什么 __dict__ 赋值或元类方案容易踩坑
有人尝试在 __new__ 里返回已有实例后,再手动把新 __dict__ 覆盖过去,这会导致对象身份混乱——id() 不同但内容被篡改,引发难以追踪的引用问题。元类方式若没正确处理 __call__ 中的锁,同样逃不开竞态。
另一个隐形陷阱:某些框架(如 pytest、Django test runner)会反复 reload 模块,导致全局锁或 _instance 变量被重置,单例“失效”不是线程问题,而是生命周期管理问题。
真正关键的从来不是“怎么写单例”,而是“是否真的需要单例”。多数时候,显式传递一个实例比全局可变状态更可控,也更容易测试。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










