- 認識單例模式
1 單例模式含義
2 單例模式優點
3 單例模式缺點
4 單例模式應用
- Python實作單例模式
1 多種實作方法
2 執行個體分析
- 總結
1.1 單例模式含義
單例模式,也叫單子模式,是一種常用的軟體設計模式。在應用這個模式時,單例對象的類必須保證隻有一個執行個體存在。許多時候整個系統隻需要擁有一個的全局對象,這樣有利于我們協調系統整體的行為。比如在某個伺服器程式中,該伺服器的配置資訊存放在一個檔案中,這些配置資料由一個單例對象統一讀取,然後服務程序中的其他對象再通過這個單例對象擷取這些配置資訊。這種方式簡化了在複雜環境下的配置管理。
實作單例模式的思路是:一個類能傳回對象一個引用(永遠是同一個)和一個獲得該執行個體的方法(必須是靜态方法,通常使用getInstance這個名稱);當我們調用這個方法時,如果類持有的引用不為空就傳回這個引用,如果類保持的引用為空就建立該類的執行個體并将執行個體的引用賦予該類保持的引用;同時我們還将該類的構造函數定義為私有方法,這樣其他處的代碼就無法通過調用該類的構造函數來執行個體化該類的對象,隻有通過該類提供的靜态方法來得到該類的唯一執行個體。
單例模式在多線程的應用場合下必須小心使用。如果當唯一執行個體尚未建立時,有兩個線程同時調用建立方法,那麼它們同時沒有檢測到唯一執行個體的存在,進而同時各自建立了一個執行個體,這樣就有兩個執行個體被構造出來,進而違反了單例模式中執行個體唯一的原則。 解決這個問題的辦法是為訓示類是否已經執行個體化的變量提供一個互斥鎖(雖然這樣會降低效率)。
1.2 單例模式優點
單例模式的優點:
1、由于單例模式要求在全局内隻有一個執行個體,因而可以節省比較多的記憶體空間;
2、全局隻有一個接入點,可以更好地進行資料同步控制,避免多重占用;
3、單例可長駐記憶體,減少系統開銷。
1.3 單例模式缺點
單例模式的缺點
1、單例模式的擴充是比較困難的;
2、賦于了單例以太多的職責,某種程度上違反單一職責原則(六大原則後面會講到);
3、單例模式是并發協作軟體子產品中需要最先完成的,因而其不利于測試;
4、單例模式在某種情況下會導緻“資源瓶頸”。
1.4 單例模式應用
單例模式的應用舉例:
1、生成全局惟一的序列号;
2、通路全局複用的惟一資源,如磁盤、總線等;
3、單個對象占用的資源過多,如資料庫等;
4、系統全局統一管理,如Windows下的Task Manager;
5、網站計數器。
2.1 多種實作方法
2.1.1.使用子產品
其實,Python 的子產品就是天然的單例模式,因為子產品在第一次導入時,會生成 .pyc 檔案,當第二次導入時,就會直接加載 .pyc 檔案,而不會再次執行子產品代碼。是以,我們隻需把相關的函數和資料定義在一個子產品中,就可以獲得一個單例對象了。如果我們真的想要一個單例類,可以考慮這樣做:
singleton_by_module.py
class Singleton(object):
def foo(self):
pass
singleton = Singleton()
将上面的代碼儲存在檔案 singleton_by_module.py 中,要使用時,直接在其他檔案中導入此檔案中的對象,這個對象即是單例模式的對象
test_singleton_by_module.py
from singleton_by_module import Singleton
t = Singleton()
這樣我們一旦調用到singleton_by_module.py就會産生一個singleton_by_module.pyc,以後我們每次調用都會直接引用這裡面的代碼。
2.1.2.使用裝飾器
singleton_by_decorator.py
def Singleton(cls):
_instance = {}
count = 0
def _singleton(*args, **kargs):
nonlocal count
if cls not in _instance:
print(f"count: {count}: {cls.__name__} not init")
_instance[cls] = cls(*args, **kargs)
else:
print(f"count: {count}: {cls.__name__} alreay init")
count+=1
return _instance[cls]
return _singleton
@Singleton
class A(object):
a = 1
def __init__(self, x=0):
self.x = x
a1 = A(2)
a2 = A(3)
print(f"a1 id: {id(a1)}, a1 value: {a1.x}")
print(f"a2 id: {id(a2)}, a2 value: {a2.x}")
output
count: 0: A not init
count: 1: A alreay init
a1 id: 140536039677232, a1 value: 2
a2 id: 140536039677232, a2 value: 2
根據上面的運作情況,我們可以發現,當a1被建立後調用的是正常的産生執行個體的過程,當a2被建立的時候,由于之前執行個體已經被存儲下來,是以直接引用了a1的執行個體,是以他們的id是一樣的,也就是他們引用了同一個記憶體執行個體。
2.1.3.使用類
singleton_by_class.py
class Singleton:
def __init__(self):
pass
@classmethod
def instance(cls, *args, **kwargs):
if not hasattr(Singleton, "_instance"):
Singleton._instance = Singleton(*args, **kwargs)
return Singleton._instance
a1 = Singleton.instance()
a2 = Singleton.instance()
print(f"a1 id: {id(a1)}")
print(f"a2 id: {id(a2)}")
a1 id: 140419818871776
a2 id: 140419818871776
一般情況,大家以為這樣就完成了單例模式,但是這樣當使用多線程時會存在問題
singleton_by_class_mutli_threading.py
def __init__(self):
pass
@classmethod
def instance(cls, *args, **kwargs):
if not hasattr(Singleton, "_instance"):
Singleton._instance = Singleton(*args, **kwargs)
return Singleton._instance
import threading
def task(arg):
obj = Singleton.instance()
print(obj)
for i in range(10):
t = threading.Thread(target=task,args=[i,])
t.start()
程式執行後,列印結果如下:
<__main__.Singleton object at 0x02C933D0>
看起來也沒有問題,那是因為執行速度過快,如果在init方法中有一些IO操作,就會發現問題了,下面我們通過time.sleep模拟
我們在上面init方法中加入以下代碼:
singleton_by_class_mutli_threading_sleep.py
def __init__(self):
import time
time.sleep(1)
重新執行程式後,結果如下
<__main__.Singleton object at 0x034A3410>
<__main__.Singleton object at 0x034BB990>
<__main__.Singleton object at 0x034BB910>
<__main__.Singleton object at 0x034ADED0>
<__main__.Singleton object at 0x034E6BD0>
<__main__.Singleton object at 0x034E6C10>
<__main__.Singleton object at 0x034E6B90>
<__main__.Singleton object at 0x034BBA30>
<__main__.Singleton object at 0x034F6B90>
<__main__.Singleton object at 0x034E6A90>
問題出現了!按照以上方式建立的單例,無法支援多線程
解決辦法:加鎖!未加鎖部分并發執行,加鎖部分串行執行,速度降低,但是保證了資料安全
singleton_by_class_mutli_threading_lock.py
import time
_instance_lock = threading.Lock()
def __init__(self):
time.sleep(1)
@classmethod
def instance(cls, *args, **kwargs):
with Singleton._instance_lock:
if not hasattr(Singleton, "_instance"):
Singleton._instance = Singleton(*args, **kwargs)
return Singleton._instance
obj = Singleton.instance()
print(obj)
t = threading.Thread(target=task,args=[i,])
t.start()
time.sleep(20)
obj = Singleton.instance()
print(obj)
列印結果如下:
<__main__.Singleton object at 0x02D6B110>
這樣就差不多了,但是還是有一點小問題,就是當程式執行時,執行了time.sleep(20)後,下面執行個體化對象時,此時已經是單例模式了,但我們還是加了鎖,這樣不太好,再進行一些優化,把intance方法,改成下面的這樣就行:
@classmethod
def instance(cls, *args, **kwargs):
if not hasattr(Singleton, "_instance"):
with Singleton._instance_lock:
if not hasattr(Singleton, "_instance"):
Singleton._instance = Singleton(*args, **kwargs)
return Singleton._instance
這樣,一個可以支援多線程的單例模式就完成了
singleton_by_class_mutli_threading_safe.py
_instance_lock = threading.Lock()
def __init__(self):
time.sleep(1)
@classmethod
def instance(cls, *args, **kwargs):
if not hasattr(Singleton, "_instance"):
with Singleton._instance_lock:
if not hasattr(Singleton, "_instance"):
Singleton._instance = Singleton(*args, **kwargs)
return Singleton._instance
obj = Singleton.instance()
print(obj)
t = threading.Thread(target=task,args=[i,])
t.start()
完整代碼
這種方式實作的單例模式,使用時會有限制,以後執行個體化必須通過 obj = Singleton.instance()
如果用 obj=Singleton() ,這種方式得到的不是單例
2.1.4基于new方法實作(推薦使用,友善)
通過上面例子,我們可以知道,當我們實作單例時,為了保證線程安全需要在内部加入鎖
我們知道,當我們執行個體化一個對象時,是先執行了類的new方法(我們沒寫時,預設調用type.new),執行個體化對象;然後再執行類的init方法,對這個對象進行初始化,所有我們可以基于這個,實作單例模式
singleton_by_new.py
_instance_lock = threading.Lock()
def __init__(self):
pass
def __new__(cls, *args, **kwargs):
if not hasattr(Singleton, "_instance"):
with Singleton._instance_lock:
if not hasattr(Singleton, "_instance"):
Singleton._instance = super(Singleton,cls).__new__(cls,*args, **kwargs)
return Singleton._instance
obj1 = Singleton()
obj2 = Singleton()
print(obj1,obj2)
obj = Singleton()
print(obj)
t = threading.Thread(target=task,args=[i,])
t.start()
<__main__.Singleton object at 0x038B33D0> <__main__.Singleton object at 0x038B33D0>
<__main__.Singleton object at 0x038B33D0>
采用這種方式的單例模式,以後執行個體化對象時,和平時執行個體化對象的方法一樣 obj = Singleton()
2.1.5.基于metaclass方式實作
相關知識
"""
1.類由type建立,建立類時,type的init方法自動執行,類() 執行type的 call方法(類的new方法,類的init方法)
2.對象由類建立,建立對象時,類的init方法自動執行,對象()執行類的 call 方法
class Foo:
def __init__(self):
pass
def __call__(self, *args, **kwargs):
pass
obj = Foo()
執行type的 call 方法,調用 Foo類(是type的對象)的 __new__方法,用于建立對象,然後調用 Foo類(是type的對象)的 __init__方法,用于對對象初始化。
obj() # 執行Foo的 call 方法
元類的使用
metaclass_ex.py
class SingletonType(type):
def __init__(self,*args,**kwargs):
super(SingletonType,self).__init__(*args,**kwargs)
def __call__(cls, *args, **kwargs): # 這裡的cls,即Foo類
print('cls',cls)
obj = cls.__new__(cls,*args, **kwargs)
cls.__init__(obj,*args, **kwargs) # Foo.__init__(obj)
return obj
class Foo(metaclass=SingletonType): # 指定建立Foo的type為SingletonType
def __init__(self,name):
self.name = name
def __new__(cls, *args, **kwargs):
return object.__new__(cls)
obj = Foo('xx')
實作單例模式
singleton_by_metaclass.py
_instance_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
if not hasattr(cls, "_instance"):
with SingletonType._instance_lock:
if not hasattr(cls, "_instance"):
cls._instance = super(SingletonType,cls).__call__(*args, **kwargs)
return cls._instance
class Foo(metaclass=SingletonType):
def __init__(self,name):
self.name = name
obj1 = Foo('name')
obj2 = Foo('name')
2.2 執行個體分析
總線是計算機各種功能部件或者裝置之間傳送資料、控制信号等資訊的公共通信解決方案之一。現假設有如下場景:某中央處理器(CPU)通過某種協定總線與一個信号燈相連,信号燈有64種顔色可以設定,中央處理器上運作着三個線程,都可以對這個信号燈進行控制,并且可以獨立設定該信号燈的顔色。抽象掉協定細節(用列印表示),如何實作線程對信号等的控制邏輯。
加線程鎖進行控制,無疑是最先想到的方法,但各個線程對鎖的控制,無疑加大了子產品之間的耦合。下面,我們就用設計模式中的單例模式,來解決這個問題。
代碼如下:
這裡使用方法__new__來實作單例模式
class Singleton(object):#抽象單例
def __new__(cls, *args, **kw):
if not hasattr(cls, '_instance'):
orig = super(Singleton, cls)
cls._instance = orig.__new__(cls, *args, **kw)
return cls._instance
總線
class Bus(Singleton):
lock = threading.RLock()
def sendData(self,data):
self.lock.acquire()
time.sleep(3)
print "Sending Signal Data...",data
self.lock.release()
線程對象,為更加說明單例的含義,這裡将Bus對象執行個體化寫在了run裡
class VisitEntity(threading.Thread):
my_bus=""
name=""
def getName(self):
return self.name
def setName(self, name):
self.name=name
def run(self):
self.my_bus=Bus()
self.my_bus.sendData(self.name)
if __name__=="__main__":
for i in range(3):
print "Entity %d begin to run..."%i
my_entity=VisitEntity()
my_entity.setName("Entity_"+str(i))
my_entity.start()
運作結果如下:
Entity 0 begin to run...
Entity 1 begin to run...
Entity 2 begin to run...
Sending Signal Data... Entity_0
Sending Signal Data... Entity_1
Sending Signal Data... Entity_2
在程式運作過程中,三個線程同時運作(運作結果的前三行先很快列印出來),而後分别占用總線資源(後三行每隔3秒列印一行)。雖然看上去總線Bus被執行個體化了三次,但實際上在記憶體裡隻有一個執行個體。
因為單例模式在設計模式中算是最基礎且最簡單的一個模式,是以在一般初級面試的時候,面試官都會通過這個問題來考察,一個很重要的原因是單例模式實作方法多種且優化的方式也有很多,是以也很能考察應聘者的水準,是以,大家要好好學這個最基礎的設計模式啊!另外,在Java中單例模式常說的飽漢餓漢模式,其實和Python中的利用__new__和利用class來建立是一樣的,也就是在什麼時候建立執行個體的差別。