python的单例模式及多线程加锁实现详解

单例模式算是最常见的设计模式了,也是面试中的高频测试点。这一篇就来总结下如何在python中实现单例模式。

单例模式

所谓单例模式,就是针对某一个类,不管实例化多少次,实例出来的对象都是同一个

之所以需要用到单例模式,有两个主要原因。其一是在程序开发中很多对象用于全局的记录,这些对象不管在程序的哪个地方被调用都应该是指向同一个,例如日志对象。其二是因为对象的创建和销毁都是耗费资源的,如果对象的创建次数减少,势必能提高程序的整体运行效率。

对象创建过程

下面先来看看python类对象的创建过程是啥样的。

平时新建一个类通常会带一个__init__()方法,如下

class MyClass:
    def __init__(self):
        pass

这里的__init__()方法的作用是对实例话的对象进行初始化,例如设定一些类变量的初始值。

但是其实在初始化之前还需要先把实例给创建出来啊,没错,这一步编译器已经替我们完成了,如果我们要手动去实现实例对象的创建可以用__new__()方法,如下

class MyClass:
    def __init__(self):
        print('initialize the object')

    def __new__(cls, *args, **kwargs):
        print('create a new object')
        return super().__new__(cls)

在所有类的基类object类中,__new__()方法接受一个类名做为参数,然后返回该类的一个空的实例对象。所以如果我们要自己去实现__new__()方法,必须用super()关键字去调用基类中的__new__()方法,并把自带的cls参数传递进去,从而获得一个本类的实例对象。

这样当类实例化的时候,首先将类通过cls参数传递到__new__()方法中,返回一个对象然后通过self参数传递到__init__()方法中。来测试下

if __name__ == '__main__':
    o = MyClass()

打印结果为

create a new object
initialize the object

值得注意的是__new__()方法只要返回一个对象即可,不一定是本类的一个实例对象,例如

class MyClass:
    def __init__(self):
        print('initialize the object')

    def __new__(cls, *args, **kwargs):
        print('create a new object')
        # return super().__new__(cls)
        return TempClass()


class TempClass:
    def __str__(self):
        return 'This is a temp class'

这里在__new__()方法里面返回的是一个TempClass的对象,再来测试下

if __name__ == '__main__':
    o = MyClass()
    print(o)

打印的结果如下

create a new object
This is a temp class

可以看到并没有执行MyClass类的__init__()方法。

假如__new__()方法没有返回任何的实例对象,则实例根本就不会创建成功,例如

class MyClass:
    def __init__(self):
        print('initialize the object')

    def __new__(cls, *args, **kwargs):
        print('create a new object')
        
     
if __name__ == '__main__':
    o = MyClass()

最后只会打印

create a new object

代码实现

好了,了解了类对象的创建过程,就可以试着实现下单例模式了。

最简单的实现代码如下

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        print('init successfully')

定义了一个类变量_instance,初始状态为None,当第一次实例化时,创建一个空实例对象,并将_instance指向该实例变量。以后再次尝试实例化时,会直接将第一次创建的实例对象返回给__init__()方法,从而达到每次都是同一个对象的目的。

下面来验证下

if __name__ == '__main__':
    s1 = Singleton()
    s2 = Singleton()
    print(id(s1))
    print(id(s2))

打印结果为

init successfully
init successfully
1410611251656
1410611251656

两个对象的内存地址相同,可见是同一个对象。

值得注意的是,虽然对象是同一个对象,但是在不同的地方可以有不同的初始化参数,例如

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, x, y):
        print('init successfully')
        self.x = x
        self.y = y


if __name__ == '__main__':
    s1 = Singleton(1, 2)
    s2 = Singleton(3, 4)
    print(id(s1))
    print(id(s2))
    print(s1.x, s1.y)
    print(s2.x, s2.y)  # 单例模式虽然实例id相同,但是init可以多遍

大家猜一猜最后打印的结果如何

init successfully
init successfully
2281646099528
2281646099528
3 4
3 4

可以看到第二次初始化将第一次初始化覆盖了,这就有点类似于全局变量,在两个地方被赋值一样。其实单例模式最初就是为了丰富全局变量的功能而出现的。

多线程

表面上看上面的代码已经达到目的了,但是其实还不够。

假设有10个线程同时要对类进行实例化,如下

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, x, y):
        # print('init successfully')
        self.x = x
        self.y = y


def func():
    obj = Singleton(1, 2)
    print(id(obj))


if __name__ == '__main__':
    for i in range(10):
        thread = threading.Thread(target=func)
        thread.start()

这样打印出来id也是一样的。

对于python多线程和多进程感兴趣的朋友可以看看另一篇博客《python多进程和多线程看这一篇就够了》

但是,假设为了模拟一个耗时的实例创建过程,人为修改一下__new__()方法如下

def __new__(cls, *args, **kwargs):
    if cls._instance is None:
        time.sleep(0.05)  # 注意这里添加了一行
        cls._instance = super().__new__(cls)
    return cls._instance

现在的打印结果如下

1818850265416
1818850265800
1818850265928
1818850266312
1818850266440
1818850266824
1818850266952
1818850267080
1818850266440
1818850266952

可见并没有实现单例模式,原因就在于0.05秒的等待时间内,10个线程在几乎都到达time.sleep(0.05)这一句,然后等待结束各自创建自己的对象。而如果不等待的化,第一个线程创建对象完毕,第二个线程就按照单例模式的逻辑直接返回了。

这其实和多线程去操作全局变量的问题一样,都需要通过加锁去解决,也就是只有获得了锁的线程才能够去开始新对象的创建逻辑,如下

class Singleton:
    _instance = None
    lock = threading.Lock()

    def __new__(cls, *args, **kwargs):
        with cls.lock:
            if cls._instance is None:
                time.sleep(0.05)
                cls._instance = super().__new__(cls)
            return cls._instance

    def __init__(self, x, y):
        # print('init successfully')
        self.x = x
        self.y = y

通过with cls.lock达到对整个逻辑加锁的目的,这之后打印的结果就是统一的了。

对于python多线程和锁感兴趣的朋友可以看看另一篇博客《python多进程和多线程看这一篇就够了》

这样子就已经挺好了,但是还不够完美。

锁的创建和释放都是消耗资源的,在上面的代码中每次创建都必须要获得锁,但是其实当对象已经创建了以后是可以直接返回不用加锁的,所以可以最后优化如下

class Singleton:
    _instance = None
    lock = threading.Lock()

    def __new__(cls, *args, **kwargs):
        if cls._instance:
            return cls._instance
        else:
            with cls.lock:
                cls._instance = super().__new__(cls)
                return cls._instance

    def __init__(self, x, y):
        # print('init successfully')
        self.x = x
        self.y = y

这样基本就是一个比较完整的单例模式实现的答案了,面试的时候这样写出来就不会有任何问题了。

import模块来实现单例模式

实际使用中会采用更为简洁的方式来实现单例模式,也就是import模块的方式。

例如有一个叫global_var.py的文件如下

class Singleton:
    pass


s = Singleton()

然后创建一个test_import.py如下

from global_var import s

print(id(s))

以及test_import2.py如下

from global_var import s
import test_import
print(id(s))

import关键字其实就是将对应的文件中的对象读入内存,假设在后续程序的运行中再次进行了import,并不会重新导入,而是直接使用内存中的内容,所以在test_import2.py中首先导入s对象,然后执行import test_import就是执行test_import.py中的内容,这里再次导入s对象,就只会直接使用内存中的s对象,从而达到单例的目的,所以打印的id结果是一致的。

而且这种方式也是线程安全的。

总结

第二种import模块的方式在很多框架的源码中都是可以看到的,实际使用中很常见,在下一篇文章中我还会以logging日志对象为例来进行实际项目使用的举例。不过面试的时候还是考察第一种方式。

我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值