单次判空无法保证线程安全,必须用双重检查锁定或模块级实例;因多线程会同时通过判空进入创建逻辑,gil不保护跨语句操作,导致重复初始化。

仅靠 __new__ 中一次 if cls._instance is None 判断无法保证线程安全,必须加锁做双重检查,或改用模块级实例。
为什么单次判空在多线程下必然失效
多个线程几乎同时执行到 if cls._instance is None 时,都看到 None,于是全部进入创建逻辑——这不是“偶尔出错”,而是确定性竞态条件。CPython 的 GIL 不保护跨语句逻辑,__new__ 本身不是原子操作。
常见错误现象包括:
- 日志里出现多次
__init__执行痕迹 - 数据库连接池重复初始化,报
ConnectionRefusedError或连接数超限 - 配置对象反复加载环境变量或文件,导致值不一致
用 threading.Lock + 双重检查锁定(DCL)
这是类内实现中最平衡的方案:兼顾性能与安全性。核心是「先无锁快查 → 再加锁细查 → 最后创建」。
实操要点:
-
_lock必须是类属性(如_lock = threading.Lock()),不能在__new__里临时创建,否则锁对象自己就可能被并发构造 - 外层
if在锁外,避免高并发下每次调用都抢锁 - 内层
if必须在with cls._lock:块内,否则仍存在窗口期 - 如果
__init__有耗时操作(如读配置、连 DB),建议也放进锁块里,否则可能被多个线程并发执行
import threading
class Logger:
_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
模块级实例才是最简单且真正线程安全的解法
Python 解释器保证模块只导入一次,且模块顶层代码执行是原子的。这意味着 instance = MyClass() 这行在首次 import 时就完成,后续所有线程拿到的都是同一个对象。
调用 Cutout.Pro 视觉处理 API 进行背景移除、人像抠图和照片增强,支持文件上传与图片 URL 输入。
它天然规避了所有锁竞争、双重检查、元类复杂度等问题,且更易测试、类型提示友好、API 清晰。
注意点:
- 初始化逻辑必须放在模块顶层(不是函数里),否则无法享受导入时的原子性
- 若需延迟初始化(比如依赖运行时参数),可在模块中配合
threading.Lock控制首次调用入口,而不是控制类本身 - 不要误用
threading.local()—— 它每个线程一个副本,和单例目标完全相反
元类实现要注意锁粒度和复用边界
当需要多个类共用同一套单例逻辑时,元类比每个类手写 __new__ 更干净。但关键在于把锁和缓存提到元类层级,而非每个类自己维护。
元类的 __call__ 是真正的构造控制点,这里加锁才有效。推荐用双重检查锁定,避免每次访问都抢锁。
容易踩的坑:
- 粗粒度锁(全程
with lock:)会导致即使实例已存在,每次调用仍要等锁,吞吐量骤降 - 元类的
_instances字典必须是类属性,且锁对象也要提前声明为类属性 - 继承该元类的子类会共享同一份缓存,若需隔离,得按类名做更细粒度键控
真正难的不是写对锁,而是判断「是否真需要单例对象」——很多场景下,模块级实例 + 显式导入,比各种 Singleton() 调用更可靠、更易维护。尤其当初始化逻辑涉及 I/O 或外部依赖时,锁的位置稍有偏差,问题就会藏得很深。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










