Python 单例模式

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

因为 foo1foo2 是两个不同的实例。

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

逻辑是:

  1. 第一次调用 Singleton() 时,_instanceNone
  2. 程序创建一个新对象,并保存到 _instance
  3. 第二次再调用 Singleton() 时,_instance 已经有值;
  4. 直接返回之前创建好的对象。

测试一下:

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__() 中,并在多线程环境下用锁保护创建过程。

相关推荐
hh.h.7 小时前
昇腾CANN ops-transformer 仓的 MC2 算子:MoE 模型的全到全通信
python·深度学习·transformer·cann
L、2187 小时前
CANN算子开发调试实战:从“Segmentation Fault“到定位根因的完整流程
java·开发语言
狗凯之家源码网7 小时前
基于PHP的多语言跨境电商B2B2C商城系统技术解析
开发语言·php
比特森林探险记8 小时前
go 语言中的context 解读和用法
开发语言·后端·golang
古城小栈8 小时前
Rust 调用 C 语言库 实战指南(企业级)
c语言·开发语言·rust
NiceCloud喜云8 小时前
Claude Files API 深入:从上传、复用到配额管理的工程化指南
android·java·数据库·人工智能·python·json·飞书
专注VB编程开发20年8 小时前
windows下python自带标准库 ≈ 70% 纯.py 源码,30% .pyd(DLL)
python
吃好睡好便好8 小时前
用for循环语句求和
开发语言·人工智能·学习·matlab·学习方法