当前位置: 首页 > ds >正文

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

一 实例的创建过程

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

  1. __new__(): 创建实例

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

    2.__init__():  初始化实例

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

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

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

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__方法。并且成功创建了实例对象

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 实例化过程

  1. 首先执行__new__(),如果没有重写__new__,默认调用object内的__new__返回一个实例对象
  2. 然后再去调用__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代码实现:

  • 这时候会发现无论创建多少次实例对象,返回的内存地址的引用不变
class Sinstance(object):obj = None"""这是一个重写__new__方法的单例类"""def __new__(cls, *args, **kwargs):if cls.obj is None:cls.obj = super().__new__(cls)return cls.objdef __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时,才会尝试获取锁并再次检查是否需要创建实例。这样可以减少锁的竞争,从而提高并发性能。

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

2.2.4 测试

线程安全

  • 这是一个创建10个线程来获取单例的方法,打印发现他们的地址引用是同一个
  • 则说明这种是线程安全的
# 测试函数:获取单例实例并打印其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()

线程不安全

import threading
import time
class Sinstance(object):obj = Nonedef __new__(cls, *args, **kwargs):if cls.obj is None:# 模拟一些工作负载time.sleep(0.001)cls.obj = object.__new__(cls)return cls.objdef __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实例对象

import threading
import time
import threadingclass Sinstance(object):_lock = threading.Lock() # 创建一个锁对象obj = Nonedef __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.objdef __init__(self):print('__init__')
te = Sinstance()

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

from pymodule import te as instance01
from pymodule import te as instance02
print(instance01)
print(instance02)
  • 这个是线程安全的

2.4 应用场景

# 1 系统缓存/软件内部配置
# 2 数据库连接池
# 3 任务调度器

http://www.xdnf.cn/news/6227.html

相关文章:

  • sqlilabs-right-Less-32(宽字节注入)
  • 自定义列甘特图,原生开发dhtmlxgantt根特图,根据数据生成只读根特图,页面展示html demo
  • 面试题-复合
  • JS,ES,TS三者什么区别
  • 【docker】--容器管理
  • GpuGeek全栈AI开发实战:从零构建企业级大模型生产管线(附完整案例)
  • 2025年Flutter初级工程师技能要求
  • fiftyone-数据库配置和config与app_config配置文件
  • 视频编解码学习十二之Android疑点
  • Git 用户名与邮箱配置全解析:精准配置——基于场景的参数选择
  • 关于并发编程AQS的学习
  • 为什么go语言中返回的指针类型,不需要用*取值(解引用),就可以直接赋值呢?
  • 什么是函数重载?为什么 C 不支持函数重载,而 C++能支持函数重载?
  • 电商平台自动化
  • 基于 Spring Boot 瑞吉外卖系统开发(十五)
  • 【MoveIt 2】使用 MoveIt 任务构造器(MoveIt Task Constructor)进行拾取和放置
  • Docker 常见问题及其解决方案
  • NLP的基本流程概述
  • uni-app vue3版本打包h5后 页面跳转报错(uni[e] is not a function)
  • 使用ECS搭建云上博客wordpress(ALMP)
  • 零基础用 Hexo + Matery 搭建博客|Github Pages 免费部署教程
  • [操作系统] 策略模式进行日志模块设计
  • OkHttp连接池
  • 5月13日日记
  • 《社交应用动态表情:RN与Flutter实战解码》
  • 场景以及八股复习篇
  • 数据清洗ETL
  • 【Python 算法零基础 2.模拟 ④ 基于矩阵】
  • 【starrocks】StarRocks 常见 HTTP 操作与导入错误排查指南
  • 数值积分知识