1 單例模式的動機
對于一個軟件系統的某些類而言,我們無須創建多個實例。舉個大家都熟知的例子——Windows任務管理器,如圖3-1所示,我們可以做一個這樣的嘗試,在Windows的“任務欄”的右鍵彈出菜單上多次點擊“啟動任務管理器”,看能否打開多個任務管理器窗口?如果你的桌面出現多個任務管理器,我請你吃飯,
(注:電腦中毒或私自修改Windows內核者除外)。通常情況下,無論我們啟動任務管理多少次,Windows系統始終只能彈出一個任務管理器窗口,也就是說在一個Windows系統中,任務管理器存在唯一性。為什么要這樣設計呢?我們可以從以下兩個方面來分析:其一,如果能彈出多個窗口,且這些窗口的內容完全一致,全部是重復對象,這勢必會浪費系統資源,任務管理器需要獲取系統運行時的諸多信息,這些信息的獲取需要消耗一定的系統資源,包括CPU資源及內存資源等,浪費是可恥的,而且根本沒有必要顯示多個內容完全相同的窗口;其二,如果彈出的多個窗口內容不一致,問題就更加嚴重了,這意味著在某一瞬間系統資源使用情況和進程、服務等信息存在多個狀態,例如任務管理器窗口A顯示“CPU使用率”為10%,窗口B顯示“CPU使用率”為15%,到底哪個才是真實的呢?這純屬“調戲”用戶,
,給用戶帶來誤解,更不可取。由此可見,確保Windows任務管理器在系統中有且僅有一個非常重要。
圖3-1 Windows任務管理器
回到實際開發中,我們也經常遇到類似的情況,為了節約系統資源,有時需要確保系統中某個類只有唯一一個實例,當這個唯一實例創建成功之后,我們無法再創建一個同類型的其他對象,所有的操作都只能基于這個唯一實例。為了確保對象的唯一性,我們可以通過單例模式來實現,這就是單例模式的動機所在。
下面我們來模擬實現Windows任務管理器,假設任務管理器的類名為TaskManager,在TaskManager類中包含了大量的成員方法,例如構造函數TaskManager(),顯示進程的方法displayProcesses(),顯示服務的方法displayServices()等,該類的示意代碼如下:
[java]view plaincopy
classTaskManager
{
publicTaskManager()?{……}//初始化窗口
publicvoiddisplayProcesses()??{……}//顯示進程
publicvoiddisplayServices()?{……}//顯示服務
……
}
為了實現Windows任務管理器的唯一性,我們通過如下三步來對該類進行重構:
(1)由于每次使用new關鍵字來實例化TaskManager類時都將產生一個新對象,為了確保TaskManager實例的唯一性,我們需要禁止類的外部直接使用new來創建對象,因此需要將TaskManager的構造函數的可見性改為private,如下代碼所示:
[java]view plaincopy
privateTaskManager()?{……}
(2)將構造函數改為private修飾后該如何創建對象呢?不要著急,雖然類的外部無法再使用new來創建對象,但是在TaskManager的內部還是可以創建的,可見性只對類外有效。因此,我們可以在TaskManager中創建并保存這個唯一實例。為了讓外界可以訪問這個唯一實例,需要在TaskManager中定義一個靜態的TaskManager類型的私有成員變量,如下代碼所示:
[java]view plaincopy
privatestaticTaskManager?tm?=null;
(3)為了保證成員變量的封裝性,我們將TaskManager類型的tm對象的可見性設置為private,但外界該如何使用該成員變量并何時實例化該成員變量呢?答案是增加一個公有的靜態方法,如下代碼所示:
[java]view plaincopy
publicstaticTaskManager?getInstance()
{
if(tm?==null)
{
tm?=newTaskManager();
}
returntm;
}
在getInstance()方法中首先判斷tm對象是否存在,如果不存在(即tm == null),則使用new關鍵字創建一個新的TaskManager類型的tm對象,再返回新創建的tm對象;否則直接返回已有的tm對象。
需要注意的是getInstance()方法的修飾符,首先它應該是一個public方法,以便供外界其他對象使用,其次它使用了static關鍵字,即它是一個靜態方法,在類外可以直接通過類名來訪問,而無須創建TaskManager對象,事實上在類外也無法創建TaskManager對象,因為構造函數是私有的。
思考
為什么要將成員變量tm定義為靜態變量?
通過以上三個步驟,我們完成了一個最簡單的單例類的設計,其完整代碼如下:
[java]view plaincopy
classTaskManager
{
privatestaticTaskManager?tm?=null;
privateTaskManager()?{……}//初始化窗口
publicvoiddisplayProcesses()?{……}//顯示進程
publicvoiddisplayServices()?{……}//顯示服務
publicstaticTaskManager?getInstance()
{
if(tm?==null)
{
tm?=newTaskManager();
}
returntm;
}
……
}
在類外我們無法直接創建新的TaskManager對象,但可以通過代碼TaskManager.getInstance()來訪問實例對象,第一次調用getInstance()方法時將創建唯一實例,再次調用時將返回第一次創建的實例,從而確保實例對象的唯一性。
上述代碼也是單例模式的一種最典型實現方式,有了以上基礎,理解單例模式的定義和結構就非常容易了。單例模式定義如下:
單例模式(Singleton Pattern):確保某一個類只有一個實例,而且自行實例化并向整個系統提供這個實例,這個類稱為單例類,它提供全局訪問的方法。單例模式是一種對象創建型模式。
單例模式有三個要點:一是某個類只能有一個實例;二是它必須自行創建這個實例;三是它必須自行向整個系統提供這個實例。
單例模式是結構最簡單的設計模式一,在它的核心結構中只包含一個被稱為單例類的特殊類。單例模式結構如圖3-2所示:
單例模式結構圖中只包含一個單例角色:
●Singleton(單例):在單例類的內部實現只生成一個實例,同時它提供一個靜態的getInstance()工廠方法,讓客戶可以訪問它的唯一實例;為了防止在外部對其實例化,將其構造函數設計為私有;在單例類內部定義了一個Singleton類型的靜態對象,作為外部共享的唯一實例。
3 負載均衡器的設計與實現
Sunny軟件公司承接了一個服務器負載均衡(Load Balance)軟件的開發工作,該軟件運行在一臺負載均衡服務器上,可以將并發訪問和數據流量分發到服務器集群中的多臺設備上進行并發處理,提高系統的整體處理能力,縮短響應時間。由于集群中的服務器需要動態刪減,且客戶端請求需要統一分發,因此需要確保負載均衡器的唯一性,只能有一個負載均衡器來負責服務器的管理和請求的分發,否則將會帶來服務器狀態的不一致以及請求分配沖突等問題。如何確保負載均衡器的唯一性是該軟件成功的關鍵。
Sunny公司開發人員通過分析和權衡,決定使用單例模式來設計該負載均衡器,結構圖如圖3-3所示:
在圖3-3中,將負載均衡器LoadBalancer設計為單例類,其中包含一個存儲服務器信息的集合serverList,每次在serverList中隨機選擇一臺服務器來響應客戶端的請求,實現代碼如下所示:
[java]view plaincopy
importjava.util.*;
//負載均衡器LoadBalancer:單例類,真實環境下該類將非常復雜,包括大量初始化的工作和業務方法,考慮到代碼的可讀性和易理解性,只列出部分與模式相關的核心代碼
classLoadBalancer?{
//私有靜態成員變量,存儲唯一實例
privatestaticLoadBalancer?instance?=null;
//服務器集合
privateList?serverList?=null;
//私有構造函數
privateLoadBalancer()?{
serverList?=newArrayList();
}
//公有靜態成員方法,返回唯一實例
publicstaticLoadBalancer?getLoadBalancer()?{
if(instance?==null)?{
instance?=newLoadBalancer();
}
returninstance;
}
//增加服務器
publicvoidaddServer(String?server)?{
serverList.add(server);
}
//刪除服務器
publicvoidremoveServer(String?server)?{
serverList.remove(server);
}
//使用Random類隨機獲取服務器
publicString?getServer()?{
Random?random?=newRandom();
inti?=?random.nextInt(serverList.size());
return(String)serverList.get(i);
}
}
編寫如下客戶端測試代碼:
[java]view plaincopy
classClient?{
publicstaticvoidmain(String?args[])?{
//創建四個LoadBalancer對象
LoadBalancer?balancer1,balancer2,balancer3,balancer4;
balancer1?=?LoadBalancer.getLoadBalancer();
balancer2?=?LoadBalancer.getLoadBalancer();
balancer3?=?LoadBalancer.getLoadBalancer();
balancer4?=?LoadBalancer.getLoadBalancer();
//判斷服務器負載均衡器是否相同
if(balancer1?==?balancer2?&&?balancer2?==?balancer3?&&?balancer3?==?balancer4)?{
System.out.println("服務器負載均衡器具有唯一性!");
}
//增加服務器
balancer1.addServer("Server?1");
balancer1.addServer("Server?2");
balancer1.addServer("Server?3");
balancer1.addServer("Server?4");
//模擬客戶端請求的分發
for(inti?=0;?i?<10;?i++)?{
String?server?=?balancer1.getServer();
System.out.println("分發請求至服務器:?"+?server);
}
}
}
編譯并運行程序,輸出結果如下:
服務器負載均衡器具有唯一性!
分發請求至服務器:Server 1
分發請求至服務器:Server 3
分發請求至服務器:Server 4
分發請求至服務器:Server 2
分發請求至服務器:Server 3
分發請求至服務器:Server 2
分發請求至服務器:Server 3
分發請求至服務器:Server 4
分發請求至服務器:Server 4
分發請求至服務器:Server 1
雖然創建了四個LoadBalancer對象,但是它們實際上是同一個對象,因此,通過使用單例模式可以確保LoadBalancer對象的唯一性。
4 餓漢式單例與懶漢式單例的討論
Sunny公司開發人員使用單例模式實現了負載均衡器的設計,但是在實際使用中出現了一個非常嚴重的問題,當負載均衡器在啟動過程中用戶再次啟動該負載均衡器時,系統無任何異常,但當客戶端提交請求時出現請求分發失敗,通過仔細分析發現原來系統中還是存在多個負載均衡器對象,導致分發時目標服務器不一致,從而產生沖突。為什么會這樣呢?Sunny公司開發人員百思不得其解。
現在我們對負載均衡器的實現代碼進行再次分析,當第一次調用getLoadBalancer()方法創建并啟動負載均衡器時,instance對象為null值,因此系統將執行代碼instance= new LoadBalancer(),在此過程中,由于要對LoadBalancer進行大量初始化工作,需要一段時間來創建LoadBalancer對象。而在此時,如果再一次調用getLoadBalancer()方法(通常發生在多線程環境中),由于instance尚未創建成功,仍為null值,判斷條件(instance== null)為真值,因此代碼instance= new LoadBalancer()將再次執行,導致最終創建了多個instance對象,這違背了單例模式的初衷,也導致系統運行發生錯誤。
如何解決該問題?我們至少有兩種解決方案,在正式介紹這兩種解決方案之前,先介紹一下單例類的兩種不同實現方式,餓漢式單例類和懶漢式單例類。
1.餓漢式單例類
餓漢式單例類是實現起來最簡單的單例類,餓漢式單例類結構圖如圖3-4所示:
從圖3-4中可以看出,由于在定義靜態變量的時候實例化單例類,因此在類加載的時候就已經創建了單例對象,代碼如下所示:
[java]view plaincopy
classEagerSingleton?{
privatestaticfinalEagerSingleton?instance?=newEagerSingleton();
privateEagerSingleton()?{?}
publicstaticEagerSingleton?getInstance()?{
returninstance;
}
}
當類被加載時,靜態變量instance會被初始化,此時類的私有構造函數會被調用,單例類的唯一實例將被創建。如果使用餓漢式單例來實現負載均衡器LoadBalancer類的設計,則不會出現創建多個單例對象的情況,可確保單例對象的唯一性。
5.懶漢式單例類與線程鎖定
除了餓漢式單例,還有一種經典的懶漢式單例,也就是前面的負載均衡器LoadBalancer類的實現方式。懶漢式單例類結構圖如圖3-5所示:
從圖3-5中可以看出,懶漢式單例在第一次調用getInstance()方法時實例化,在類加載時并不自行實例化,這種技術又稱為延遲加載(Lazy Load)技術,即需要的時候再加載實例,為了避免多個線程同時調用getInstance()方法,我們可以使用關鍵字synchronized,代碼如下所示:
[java]view plaincopy
classLazySingleton?{
privatestaticLazySingleton?instance?=null;
privateLazySingleton()?{?}
synchronizedpublicstaticLazySingleton?getInstance()?{
if(instance?==null)?{
instance?=newLazySingleton();
}
returninstance;
}
}
該懶漢式單例類在getInstance()方法前面增加了關鍵字synchronized進行線程鎖,以處理多個線程同時訪問的問題。但是,上述代碼雖然解決了線程安全問題,但是每次調用getInstance()時都需要進行線程鎖定判斷,在多線程高并發訪問環境中,將會導致系統性能大大降低。如何既解決線程安全問題又不影響系統性能呢?我們繼續對懶漢式單例進行改進。事實上,我們無須對整個getInstance()方法進行鎖定,只需對其中的代碼“instance = new LazySingleton();”進行鎖定即可。因此getInstance()方法可以進行如下改進:
[java]view plaincopy
publicstaticLazySingleton?getInstance()?{
if(instance?==null)?{
synchronized(LazySingleton.class)?{
instance?=newLazySingleton();
}
}
returninstance;
}
問題貌似得以解決,事實并非如此。如果使用以上代碼來實現單例,還是會存在單例對象不唯一。原因如下:
假如在某一瞬間線程A和線程B都在調用getInstance()方法,此時instance對象為null值,均能通過instance == null的判斷。由于實現了synchronized加鎖機制,線程A進入synchronized鎖定的代碼中執行實例創建代碼,線程B處于排隊等待狀態,必須等待線程A執行完畢后才可以進入synchronized鎖定代碼。但當A執行完畢時,線程B并不知道實例已經創建,將繼續創建新的實例,導致產生多個單例對象,違背單例模式的設計思想,因此需要進行進一步改進,在synchronized中再進行一次(instance == null)判斷,這種方式稱為雙重檢查鎖定(Double-Check Locking)。使用雙重檢查鎖定實現的懶漢式單例類完整代碼如下所示:
[java]view plaincopy
classLazySingleton?{
privatevolatilestaticLazySingleton?instance?=null;
privateLazySingleton()?{?}
publicstaticLazySingleton?getInstance()?{
//第一重判斷
if(instance?==null)?{
//鎖定代碼塊
synchronized(LazySingleton.class)?{
//第二重判斷
if(instance?==null)?{
instance?=newLazySingleton();//創建單例實例
}
}
}
returninstance;
}
}
需要注意的是,如果使用雙重檢查鎖定來實現懶漢式單例類,需要在靜態成員變量instance之前增加修飾符volatile,被volatile修飾的成員變量可以確保多個線程都能夠正確處理,且該代碼只能在JDK 1.5及以上版本中才能正確執行。由于volatile關鍵字會屏蔽Java虛擬機所做的一些代碼優化,可能會導致系統運行效率降低,因此即使使用雙重檢查鎖定來實現單例模式也不是一種完美的實現方式。
擴展
IBM公司高級軟件工程師Peter??? Haggar 2004年在IBM developerWorks上發表了一篇名為《雙重檢查鎖定及單例模式——全面理解這一失效的編程習語》的文章,對JDK??? 1.5之前的雙重檢查鎖定及單例模式進行了全面分析和闡述,參考鏈接:http://www.ibm.com/developerworks/cn/java/j-dcl.html
6.餓漢式單例類與懶漢式單例類比較
餓漢式單例類在類被加載時就將自己實例化,它的優點在于無須考慮多線程訪問問題,可以確保實例的唯一性;從調用速度和反應時間角度來講,由于單例對象一開始就得以創建,因此要優于懶漢式單例。但是無論系統在運行時是否需要使用該單例對象,由于在類加載時該對象就需要創建,因此從資源利用效率角度來講,餓漢式單例不及懶漢式單例,而且在系統加載時由于需要創建餓漢式單例對象,加載時間可能會比較長。
懶漢式單例類在第一次使用時創建,無須一直占用系統資源,實現了延遲加載,但是必須處理好多個線程同時訪問的問題,特別是當單例類作為資源控制器,在實例化時必然涉及資源初始化,而資源初始化很有可能耗費大量時間,這意味著出現多線程同時首次引用此類的機率變得較大,需要通過雙重檢查鎖定等機制進行控制,這將導致系統性能受到一定影響。
7 一種更好的單例實現方法
餓漢式單例類不能實現延遲加載,不管將來用不用始終占據內存;懶漢式單例類線程安全控制煩瑣,而且性能受影響。可見,無論是餓漢式單例還是懶漢式單例都存在這樣那樣的問題,有沒有一種方法,能夠將兩種單例的缺點都克服,而將兩者的優點合二為一呢?答案是:Yes!下面我們來學習這種更好的被稱之為Initialization Demand Holder (IoDH)的技術。
在IoDH中,我們在單例類中增加一個靜態(static)內部類,在該內部類中創建單例對象,再將該單例對象通過getInstance()方法返回給外部使用,實現代碼如下所示:
[java]view plaincopy
//Initialization?on?Demand?Holder
classSingleton?{
privateSingleton()?{
}
privatestaticclassHolderClass?{
privatefinalstaticSingleton?instance?=newSingleton();
}
publicstaticSingleton?getInstance()?{
returnHolderClass.instance;
}
publicstaticvoidmain(String?args[])?{
Singleton?s1,?s2;
s1?=?Singleton.getInstance();
s2?=?Singleton.getInstance();
System.out.println(s1==s2);
}
}
編譯并運行上述代碼,運行結果為:true,即創建的單例對象s1和s2為同一對象。由于靜態單例對象沒有作為Singleton的成員變量直接實例化,因此類加載時不會實例化Singleton,第一次調用getInstance()時將加載內部類HolderClass,在該內部類中定義了一個static類型的變量instance,此時會首先初始化這個成員變量,由Java虛擬機來保證其線程安全性,確保該成員變量只能初始化一次。由于getInstance()方法沒有任何線程鎖定,因此其性能不會造成任何影響。
通過使用IoDH,我們既可以實現延遲加載,又可以保證線程安全,不影響系統性能,不失為一種最好的Java語言單例模式實現方式(其缺點是與編程語言本身的特性相關,很多面向對象語言不支持IoDH)。
8 單例模式總結
單例模式作為一種目標明確、結構簡單、理解容易的設計模式,在軟件開發中使用頻率相當高,在很多應用軟件和框架中都得以廣泛應用。
1.主要優點
單例模式的主要優點如下:
(1)單例模式提供了對唯一實例的受控訪問。因為單例類封裝了它的唯一實例,所以它可以嚴格控制客戶怎樣以及何時訪問它。
(2)由于在系統內存中只存在一個對象,因此可以節約系統資源,對于一些需要頻繁創建和銷毀的對象單例模式無疑可以提高系統的性能。
(3)允許可變數目的實例。基于單例模式我們可以進行擴展,使用與單例控制相似的方法來獲得指定個數的對象實例,既節省系統資源,又解決了單例單例對象共享過多有損性能的問題。
2.主要缺點
單例模式的主要缺點如下:
(1)由于單例模式中沒有抽象層,因此單例類的擴展有很大的困難。
(2)單例類的職責過重,在一定程度上違背了“單一職責原則”。因為單例類既充當了工廠角色,提供了工廠方法,同時又充當了產品角色,包含一些業務方法,將產品的創建和產品的本身的功能融合到一起。
(3)現在很多面向對象語言(如Java、C#)的運行環境都提供了自動垃圾回收的技術,因此,如果實例化的共享對象長時間不被利用,系統會認為它是垃圾,會自動銷毀并回收資源,下次利用時又將重新實例化,這將導致共享的單例對象狀態的丟失。
3.適用場景
在以下情況下可以考慮使用單例模式:
(1)系統只需要一個實例對象,如系統要求提供一個唯一的序列號生成器或資源管理器,或者需要考慮資源消耗太大而只允許創建一個對象。
(2)客戶調用類的單個實例只允許使用一個公共訪問點,除了該公共訪問點,不能通過其他途徑訪問該實例。
注:非原創,不裝逼,不虛假,整合分享