简单介绍
单例模式
是一种非常常用的软件设计模式,它定义是单例对象的类只能允许一个实例存在
。
单例模式有两种经典的实现方式饿汉模式
和懒汉模式
, 接下来以它们为开端来介绍单例模式.
懒汉模式
顾名思义, 创建对象时比较懒, 到需要用到时才去创建.
使用__new__
类方法实现, 每次实例化类时会判断是否已有类属性_instance
(里面存的是已经实例化的类对象), 有的话直接返回, 没有就创建新实例.
class Singleton:
def __init__(self): pass
def __new__(cls): if not hasattr(cls, '_instance'): cls._instance = super().__new__(cls) return cls._instance
if __name__ == "__main__": S1 = Singleton() S2 = Singleton() print(id(S1)) print(id(S2))
|
输出结果
139789593957088 139789593957088
|
但这样存在一个问题, 如果同时实例化多个类, if not hasattr(cls, '_instance')
语句有可能同时成立, 这会造成创建多个新的实例, 比如在多线程操作下.
import time from threading import Thread
class Singleton:
def __init__(self): pass
def __new__(cls): if not hasattr(cls, '_instance'): time.sleep(1) cls._instance = super().__new__(cls) return cls._instance
def foo():
S = Singleton() print(id(S))
if __name__ == "__main__": thread_array = [Thread(target=foo) for _ in range(2)] [t.start() for t in thread_array]
|
输出结果
139858525528848 139858525530288
|
这是因为多线程下的操作并不是原子性的, 解决方法也很简单, 加上互斥锁将__new__
方法里的操作转变成原子性就好了.
import time from threading import Lock, Thread
class Singleton: lock = Lock()
def __init__(self): pass
def __new__(cls): with cls.lock: if not hasattr(cls, '_instance'): time.sleep(1) cls._instance = super().__new__(cls) return cls._instance
def foo(): S = Singleton() print(id(S))
if __name__ == "__main__": thread_array = [Thread(target=foo) for _ in range(2)] [t.start() for t in thread_array]
|
输出结果
140572949160720 140572949160720
|
这样就没问题了吧? 不, 我们还需要对__init__
函数做处理, 比方说以下的情况, 每次实例化虽不会创建新实例, 当会执行一遍初始化里的代码, 这是不久单例了个寂寞吗?
import time from threading import Lock, Thread
class Singleton: lock = Lock()
def __init__(self): print('初始化中') print('初始化完毕')
def __new__(cls): with cls.lock: if not hasattr(cls, '_instance'): time.sleep(1) cls._instance = super().__new__(cls) return cls._instance
def foo(): S = Singleton() print(id(S))
if __name__ == "__main__": thread_array = [Thread(target=foo) for _ in range(2)] [t.start() for t in thread_array]
|
输出结果
初始化中 初始化完毕 140424166351632 初始化中 初始化完毕 140424166351632
|
为了优化这一问题, 在__init__
里加个判断就行了.
def __init__(self): if hasattr(self, '_init'): return self._init = True
print('初始化中') print('初始化完毕')
|
到了这一步完整的懒汉模式就已经实现了, 但是, 还有个细节可以优化, 这里引入双重检查锁
的概念.
双重检查锁
双重检查锁定模式首先验证锁定条件(第一次检查),只有通过锁定条件验证才真正的进行加锁逻辑并再次验证条件(第二次检查)。
这样会在保证性能的同时又保证单例. (小声BB: python还需要考虑性能? 不, 这是对细节的把控!)
def __new__(cls): if not hasattr(cls, '_instance'): with cls.lock: if not hasattr(cls, '_instance'): cls._instance = super().__new__(cls) return cls._instance
|
最终代码
from threading import Lock, Thread
class Singleton: lock = Lock()
def __init__(self): if hasattr(self, '_init'): return self._init = True
def __new__(cls): if not hasattr(cls, '_instance'): with cls.lock: if not hasattr(cls, '_instance'): cls._instance = super().__new__(cls) return cls._instance
|
饿汉模式
提前创建好对象, 要用到就直接使用, 不会被'饿'死.
python 和 Java 不同, 没办法在类中实例化本身, 所以传统的实现方式行不通, 这里我认为比较合理的实现应该是:
class Singleton:
def __init__(self): pass Singleton = Singleton()
|
直接进行实例化, 然后调用实例化对象, 欸, 这不就是正常的调用类的方式吗? 确实, 但它却满足了饿汉模式实现的条件, 权当了解即可.
两者的优缺点
饿汉模式
:优点是没有线程安全的问题,缺点是浪费内存空间。
懒汉模式
:优点是没有内存空间浪费的问题,缺点是如果控制不好,实际上不是单例的。