[python] Python单例模式:__new__与线程安全解析

一 实例的创建过程

我们之前了解过在构造一个类的实例化对象时,会默认调用__init__方法,也就是类的初始化也叫构造函数,但其实在调用__init__方法前会首先调用__new__方法(只有在py3新式类才有)。即下面

  1. new(): 创建实例

作用: 在内存中分配对象空间 2 返回对象的引用self传递给init方法

2.init(): 初始化实例

  • 当我们手动重写这个方法后发现 构造函数没有被调用了,并且调用test会报错

  • 此时我们调用查看构造的对象 ,发现它其实就是None

  • 因为new方法被重写了,并没有创建对象。也没有分配资源空间

python 复制代码
class Test(object):
    def __init__(self):
        print("__init__")
    def __new__(cls, *args, **kwargs):
        print("__new__")
    def test(self):
        print("test")
test = Test()
test.test()

此时我们重写__new__方法

1.1 重点(重写__new__)

  • 重写__new__方法时一定要返回父类的__new__方法否则无法成功分配内存,

    return super().new

  • 这时候发现首先调用了__new__方法,然后调用了__init__方法。并且成功创建了实例对象

python 复制代码
class Test(object):
    def __init__(self):
        print("__init__")
    def __new__(cls, *args, **kwargs):
        print("__new__")
        res = super().__new__(cls,*args, **kwargs)
        return res
test = Test()
print(test)

接下来看看一个类实例化的过程

1.2 实例化过程

复制代码
首先执行__new__(),如果没有重写__new__,默认调用object内的__new__返回一个实例对象
复制代码
然后再去调用__init__去初始化对象

__new__是创建对象,分配空间等, __init__是初始化对象

__new__是返回对象引用,__init__是定义实例属性

__new__是类级别的方法,__init__是实例级别的方法

二 单例

单例是软件23种设计模式之一,一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问这个实例

2.1 单例创建的几种方式

1 通过@classmethmod类方法

2 通过装饰器

3 通过重写__new__ (主要方法)

4 通过导入模块

2.2 通过重写__new__方法实现

2.2.1设计流程:

1 定义一个类属性,初始值为None,用来记录单例对象的引用

2 重写__new__方法

3 进行判断,如果类属性是None,把__new__返回的对象引用保存进去

4 返回类属性中记录的对象引用

2.2.2代码实现:

  • 这时候会发现无论创建多少次实例对象,返回的内存地址的引用不变
python 复制代码
class Sinstance(object):
    obj = None
    """这是一个重写__new__方法的单例类"""
    def __new__(cls, *args, **kwargs):
        if cls.obj is None:
            cls.obj = super().__new__(cls)
        return cls.obj
    def __init__(self):
        print("__init__")

s = Sinstance()
s2 = Sinstance()
print(s)
print(s2)

2.2.3 线程安全问题

  • 上面这种方式在遇到多线程访问时就会出现线程不安全。
  • 两个线程可能同时执行到if cls.obj is None:这一行检查,发现cls.obj都为None,然后各自创建一个新实例,这就破坏了单例模式的目标。
  • 为了确保在多线程环境下的线程安全性,你需要引入某种形式的同步机制来防止多个线程同时进入创建实例的代码块。最常见的做法是使用锁(Lock)。
  • 这种情况只会在第一次创建对象时有加锁解锁的额外开销,并不会对性能有太大的影响

在这个版本中,我们使用了双重检查锁定模式:首先不加锁进行一次检查,如果obj还未被初始化,则获取锁后(上锁)(再此检查是担心有别的线程在这个线程还会加锁的时候完成了实例创建),再检查一次后,并在此时真正地创建实例。这样做不仅保证了线程安全性,还提高了性能,因为大多数情况下不会进入加锁的代码段。只有当obj确实为None时,才会尝试获取锁并再次检查是否需要创建实例。这样可以减少锁的竞争,从而提高并发性能。

cpp 复制代码
import threading

class Sinstance(object):
    _lock = threading.Lock() # 创建一个锁对象
    obj = None
    def __new__(cls, *args, **kwargs):
        if cls.obj is None:
            with cls._lock: #在此处加锁
                if cls.obj is None: #双重检查锁定,避免不必要的加锁开销
                    cls.obj = object.__new__(cls)
        return cls.obj
    def __init__(self):
        print('__init__')

2.2.4 测试

线程安全

  • 这是一个创建10个线程来获取单例的方法,打印发现他们的地址引用是同一个
  • 则说明这种是线程安全的
python 复制代码
# 测试函数:获取单例实例并打印其ID
def test_singleton():
    instance = Sinstance()
    print(f"Instance ID: {id(instance)}")

threads = []
for _ in range(10):
    t = threading.Thread(target=test_singleton)
    threads.append(t)
    t.start()
# 等待所有线程完成
for t in threads:
    t.join()

线程不安全

python 复制代码
import threading
import time
class Sinstance(object):
    obj = None
    def __new__(cls, *args, **kwargs):
        if cls.obj is None:
            # 模拟一些工作负载
           time.sleep(0.001)
           cls.obj = object.__new__(cls)
        return cls.obj
    def __init__(self):
        print('__init__')

# 测试函数:获取单例实例并打印其ID
def test_singleton():
    instance = Sinstance()
    print(f"Instance ID: {id(instance)}")

threads = []
for _ in range(10):
    t = threading.Thread(target=test_singleton)
    threads.append(t)
    t.start()
# 等待所有线程完成
for t in threads:
    t.join()
  • 经过测试如果不加锁,确实线程是不安全的

2.3 通过模块导入的方式

利用模块导入的方式实现单例模式,在Python中实际上是一种非常简单且线程安全的方法。这是因为在Python中,模块在第一次被导入时会执行其顶层代码,并且Python的模块导入机制保证了每个模块只会被加载一次,即使多次导入同一个模块,也只会执行一次模块中的代码。这种特性天然地支持了单例模式的需求。

  • 这是因为 Python 的模块只会被加载一次,即使你多次导入同一个模块,
  • 在后续的导入操作中,Python只是重复使用已经加载的模块对象。
  • 这个在多线程的方式下是安全的.

首先我们再pymodule.py文件中创建这个te实例对象

python 复制代码
import threading
import time
import threading

class Sinstance(object):
    _lock = threading.Lock() # 创建一个锁对象
    obj = None
    def __new__(cls, *args, **kwargs):
        if cls.obj is None:
            time.sleep(0.001)
            with cls._lock: #在此处加锁
                if cls.obj is None: #双重检查锁定,避免不必要的加锁开销
                    cls.obj = object.__new__(cls)
        return cls.obj
    def __init__(self):
        print('__init__')
te = Sinstance()

接着我们再另一个py文件里调用

python 复制代码
from pymodule import te as instance01
from pymodule import te as instance02
print(instance01)
print(instance02)
  • 这个是线程安全的

2.4 应用场景

1 系统缓存/软件内部配置

2 数据库连接池

3 任务调度器

相关推荐
漫路在线25 分钟前
JS逆向-某易云音乐下载器
开发语言·javascript·爬虫·python
小辉懂编程1 小时前
C语言:51单片机实现数码管依次循环显示【1~F】课堂练习
c语言·开发语言·51单片机
醍醐三叶2 小时前
C++类与对象--2 对象的初始化和清理
开发语言·c++
Magnum Lehar3 小时前
3d游戏引擎EngineTest的系统实现3
java·开发语言·游戏引擎
Mcworld8573 小时前
java集合
java·开发语言·windows
成功人chen某3 小时前
配置VScodePython环境Python was not found;
开发语言·python
2301_786964363 小时前
EXCEL Python 实现绘制柱状线型组合图和树状图(包含数据透视表)
python·microsoft·excel
skd89994 小时前
小蜗牛拨号助手用户使用手册
python
「QT(C++)开发工程师」4 小时前
STM32 | FreeRTOS 递归信号量
python·stm32·嵌入式硬件
动感光博4 小时前
Unity序列化字段、单例模式(Singleton Pattern)
unity·单例模式·c#