一、说明
1、定义:
单例模式是所有设计模式中比较简单的一类,其定义如下:Ensure a class has only one instance, and provide a global point of access to it.(保证某一个类只有一个实例,而且在全局只有一个访问点)。
2、优缺点
优点:
1、由于单例模式要求在全局内只有一个实例,因而可以节省比较多的内存空间;
2、全局只有一个接入点,可以更好地进行数据同步控制,避免多重占用;
3、单例可长驻内存,减少系统开销。
缺点:
1、单例模式的扩展是比较困难的;
2、赋于了单例以太多的职责,某种程度上违反单一职责原则
3、场景
最常用的实例就是当一个office文档已经被打开时,通过另外一个窗口再次访问时,系统会提示已经被此文档已经被占用,只能以只读方式打开。
二、代码实现
1、方式一:常规方式实现单例模式
# 方式一
class Singleton(object):
instance = None
def __init__(self, name):
self.name = name
def __new__(cls, *args, **kwargs): # 返回空对象
if cls.instance: # 是否已创建过对象
return cls.instance # 如果有,则直接返回
cls.instance = object.__new__(cls) # 没有则创建空对象,返回该空对象
return cls.instance
obj1 = Singleton("asd")
obj2 = Singleton("a")
print(obj2.name, obj1.name)
print(id(obj1)==id(obj2))
以上为类Singleton
添加关键属性instance
,该属性用于如果为空表示该类还未创建实例,如果不为空,则说明已经该类已经实例化过。__new__
方法会创建一个Singleton
的空对象,在创建过程中添加判断类属性instance
是否已绑定对象实例的逻辑。
以上代码执行结果如下:
a a
True
两个obj具有同样的name属性和地址,说明是一个对象
2、方式二:继承单例类型
为了满足自定义一些方法,扩展性更高,使用继承修改一下单例模式
# 方式二
class Singleton(object):
instance = None
def __init__(self, name):
self.name = name
def __new__(cls, *args, **kwargs):
# 返回空对象
if cls.instance:
return cls.instance
cls.instance = object.__new__(cls) # 创建空对象
return cls.instance
# 继承方式改写单例模式
class MySingleton(Singleton):
def func(self):
pass
obj3 = MySingleton("my singleton")
obj4 = MySingleton("my singleton")
print(id(obj4)==id(obj3)) # True
3、方式三:多线程改写单例模式
以上两种方式属于虽然功能正常,但是在多线程情景下,可能存在由于单例类创建空对象的时候非原子操作,所以一个线程创建单例调用__new__
方法返回空对象的过程中,其他线程也在执行,也就是出现了多个线程同时拿到了Singleton.instance=None
的情形,因而返回了多个不同的新的空对象,导致单例模式失效。代码如下
import time
import threading
# 单例模式常规写法多线程下的问题
class Singleton(object):
instance = None
def __init__(self, name):
self.name = name
def __new__(cls, *args, **kwargs):
if cls.instance:
return cls.instance
time.sleep(0.01) # 放慢线程创建空对象的速度
cls.instance = object.__new__(cls)
return cls.instance
def task(n):
obj = Singleton("singleton by %dth thread" % n)
print("thread %s, id=%s" % (obj.name, id(obj)))
for i in range(10):
t = threading.Thread(target=task, args=(i+1,))
t.start()
以上引入threading
模块创建多个线程,在__new__
方法中使用time.sleep(0.01)
语句阻塞单个线程创建空对象从而模拟极端情况。
执行结果如下
singleton by 1th thread, id=2475130212192
singleton by 2th thread, id=2475130212248
singleton by 4th thread, id=2474978871000
singleton by 6th thread, id=2474978871392
singleton by 5th thread, id=2475128184224
singleton by 3th thread, id=2474978871336
singleton by 7th thread, id=2474978871000
singleton by 9th thread, id=2475128184224
singleton by 10th thread, id=2474978871336
singleton by 8th thread, id=2474978871392
可看到不同的线程创建单例顺序不同,地址也不同。单例模式失效。
究其原因是因为,__new__
方法可以由多个线程同时执行,所以可以使用锁机制控制。代码如下方式三
# 方式三:多线程版本下的单例模式
import time
import threading
lock = threading.RLock()
# 多线程情景下的单例模式
class Singleton(object):
instance = None
def __init__(self, name):
self.name = name
def __new__(cls, *args, **kwargs):
with lock:
if cls.instance:
return cls.instance
time.sleep(0.1)
cls.instance = object.__new__(cls) # 创建空对象
return cls.instance
def task(n):
obj = Singleton("%dth singleton" % n)
print("%s, id=%s" % (obj.name, id(obj)))
for i in range(10):
t = threading.Thread(target=task, args=(i+1,))
t.start()
如上,采用锁机制解决问题资源同步和数据一致性的问题。当多个线程尝试创建对象的时候,都会先请求锁,拿到锁则执行,否则只能等待。其实等于变相使用锁机制控制多线程对同一资源instance
类变量的访问。
代码结果如下:
singleton by 1th thread, id=2090604208704
singleton by 2th thread, id=2090604208704
singleton by 3th thread, id=2090604208704
singleton by 4th thread, id=2090604208704
singleton by 5th thread, id=2090604208704
singleton by 6th thread, id=2090604208704
singleton by 7th thread, id=2090604208704
singleton by 8th thread, id=2090604208704
singleton by 9th thread, id=2090604208704
singleton by 10th thread, id=2090604208704
以上使用线程锁,采用with
上下文管理管理的方式,同样可以使用手动获取锁,释放的方式实现如下。
lock.acquire()
if cls.instance:
return cls.instance
time.sleep(0.1)
cls.instance = object.__new__(cls) # 创建空对象
return cls.instance
lock.release()
Python中的线程锁:
- RLock:
threading.RLock()
,支持锁的嵌套。 - Lock:
threading.RLock()
,不支持锁的嵌套,效率比RLock稍微高一些。
如果一个函数内部操作用了Lock,那么再调用该函数就无法再上锁了,因此虽然RLock资源消耗稍微高一点,但是用Rlock多一些。
4、方式四:优化后多线程下的单例模式
# 方式四:优化后多线程下的单例模式
class Singleton(object):
instance = None
lock = threading.RLock()
def __init__(self, name):
self.name = name
def __new__(cls, *args, **kwargs):
# 性能优化一点
if cls.instance:
return cls.instance
with cls.lock:
if cls.instance:
return cls.instance
cls.instance = object.__new__(cls)
return cls.instance
以上方式四与方式三改进的两点在于:
一:lock锁应该作为单例类型的类属性,因此将lock = threading.RLock()
放到了类中。
二:在__new__
方法中添加if cls.instance: return cls.instance
使得后续再重新创建实例如new_obj=Singleton("new obj")
的时候,不需要再进入with cls.lock:
代码逻辑中耗费一次锁资源。