__new__中判空非原子操作会导致多线程重复创建实例;DCL通过无锁初判+加锁后复判解决,需配合__init__幂等防护。
__new__ 中的 if 判断不是原子操作
线程 A 执行到 if cls._instance is None 时为 True,刚准备调用 super().__new__(cls),此时被调度挂起;线程 B 同样判空成功,也进入创建逻辑——结果两个线程各自生成实例并赋值给 _instance。GIL 不管这个:它只保单个字节码原子,不保「判断 + 赋值」这种跨语句逻辑。
常见错误现象包括:
-
__init__被执行多次(日志里看到两行“初始化配置”) - 数据库连接对象出现多个副本,引发
ConnectionRefusedError或连接数超限 - 全局状态(如缓存字典、计数器)被并发写乱
为什么加一个 threading.Lock 就能修好
锁的作用不是“让 __new__ 变成原子”,而是把整个检查 + 创建路径变成临界区。但直接包死整个 __new__ 会严重拖慢高并发性能——每次获取实例都要抢锁。
所以得用双重检查锁定(DCL),核心是三步:
- 先无锁快速判断
cls._instance is None,绝大多数时候直接返回,不进锁 - 仅当为空时,才进
with cls._lock:块 - 进锁后必须再查一次
cls._instance is None,防止多个线程排队等锁时,第一个已建好,后面几个还傻乎乎重建
锁对象本身必须是类属性(_lock = threading.Lock()),不能在方法里临时创建,否则锁对象自己就可能被并发构造。
threading.Lock 和 threading.RLock 的区别在哪
对单例场景来说,用 threading.Lock 就够了。RLock 允许同一线程重复 acquire,但单例构造逻辑里不存在递归调用或重入需求,反而多一层开销。
Python 3.14.3
微软官方的 Python 扩展,是 VS Code 安装量最高的扩展(209M+)。集成 IntelliSense(通过 Pylance)、调试(通过 Python Debugger)、代码检查、格式化、重构和单元测试等功能。支持 Jupyter Notebook、虚拟环境管理和多 Python 版本切换。
关键点在于:
-
Lock更轻量,acquire 失败直接阻塞,语义清晰 -
RLock会维护持有者和计数,单例里纯属冗余 - 别误用
threading.Semaphore(1)替代Lock,语义不同,且容易漏 release
__new__ 里传 *args/**kwargs 会报错
这是新手高频翻车点:super().__new__(cls, *args, **kwargs) 会抛 TypeError: object() takes no parameters。因为 object.__new__ 只接受 cls,不接受任何其他参数。
正确做法是:
-
__new__只负责分配内存,只传cls - 所有参数处理、属性初始化、副作用逻辑(比如读配置文件、建连接)全放
__init__里 - 如果
__init__可能被多次调用(DCL 没守卫时),加if not hasattr(self, '_initialized'):控制
最容易被忽略的是:即使你写了完美的 DCL,只要 __init__ 没防护,照样会执行多次——单例的“唯一性”不止在实例对象,更在初始化逻辑的幂等性。
就爱读