确保对象的唯一性——单例模式
本文轉(zhuǎn)載自 :http://blog.csdn.net/lovelion/article/details/7420883
3.1 單例模式的動機
??????對于一個軟件系統(tǒng)的某些類而言,我們無須創(chuàng)建多個實例。舉個大家都熟知的例子——Windows任務(wù)管理器,如圖3-1所示,我們可以做一個這樣的嘗試,在Windows的“任務(wù)欄”的右鍵彈出菜單上多次點擊“啟動任務(wù)管理器”,看能否打開多個任務(wù)管理器窗口?如果你的桌面出現(xiàn)多個任務(wù)管理器,我請你吃飯,(注:電腦中毒或私自修改Windows內(nèi)核者除外)。通常情況下,無論我們啟動任務(wù)管理多少次,Windows系統(tǒng)始終只能彈出一個任務(wù)管理器窗口,也就是說在一個Windows系統(tǒng)中,任務(wù)管理器存在唯一性。為什么要這樣設(shè)計呢?我們可以從以下兩個方面來分析:其一,如果能彈出多個窗口,且這些窗口的內(nèi)容完全一致,全部是重復對象,這勢必會浪費系統(tǒng)資源,任務(wù)管理器需要獲取系統(tǒng)運行時的諸多信息,這些信息的獲取需要消耗一定的系統(tǒng)資源,包括CPU資源及內(nèi)存資源等,浪費是可恥的,而且根本沒有必要顯示多個內(nèi)容完全相同的窗口;其二,如果彈出的多個窗口內(nèi)容不一致,問題就更加嚴重了,這意味著在某一瞬間系統(tǒng)資源使用情況和進程、服務(wù)等信息存在多個狀態(tài),例如任務(wù)管理器窗口A顯示“CPU使用率”為10%,窗口B顯示“CPU使用率”為15%,到底哪個才是真實的呢?這純屬“調(diào)戲”用戶,,給用戶帶來誤解,更不可取。由此可見,確保Windows任務(wù)管理器在系統(tǒng)中有且僅有一個非常重要。
?????????圖3-1 Windows任務(wù)管理器
??????回到實際開發(fā)中,我們也經(jīng)常遇到類似的情況,為了節(jié)約系統(tǒng)資源,有時需要確保系統(tǒng)中某個類只有唯一一個實例,當這個唯一實例創(chuàng)建成功之后,我們無法再創(chuàng)建一個同類型的其他對象,所有的操作都只能基于這個唯一實例。為了確保對象的唯一性,我們可以通過單例模式來實現(xiàn),這就是單例模式的動機所在。
?
3.2?單例模式概述
??????下面我們來模擬實現(xiàn)Windows任務(wù)管理器,假設(shè)任務(wù)管理器的類名為TaskManager,在TaskManager類中包含了大量的成員方法,例如構(gòu)造函數(shù)TaskManager(),顯示進程的方法displayProcesses(),顯示服務(wù)的方法displayServices()等,該類的示意代碼如下:
[java]?view plaincopy???????為了實現(xiàn)Windows任務(wù)管理器的唯一性,我們通過如下三步來對該類進行重構(gòu):
????? (1)??由于每次使用new關(guān)鍵字來實例化TaskManager類時都將產(chǎn)生一個新對象,為了確保TaskManager實例的唯一性,我們需要禁止類的外部直接使用new來創(chuàng)建對象,因此需要將TaskManager的構(gòu)造函數(shù)的可見性改為private,如下代碼所示:
[java]?view plaincopy?????? (2)??將構(gòu)造函數(shù)改為private修飾后該如何創(chuàng)建對象呢?不要著急,雖然類的外部無法再使用new來創(chuàng)建對象,但是在TaskManager的內(nèi)部還是可以創(chuàng)建的,可見性只對類外有效。因此,我們可以在TaskManager中創(chuàng)建并保存這個唯一實例。為了讓外界可以訪問這個唯一實例,需要在TaskManager中定義一個靜態(tài)的TaskManager類型的私有成員變量,如下代碼所示:
[java]?view plaincopy??????(3)??為了保證成員變量的封裝性,我們將TaskManager類型的tm對象的可見性設(shè)置為private,但外界該如何使用該成員變量并何時實例化該成員變量呢?答案是增加一個公有的靜態(tài)方法,如下代碼所示:
[java]?view plaincopy?????? 在getInstance()方法中首先判斷tm對象是否存在,如果不存在(即tm == null),則使用new關(guān)鍵字創(chuàng)建一個新的TaskManager類型的tm對象,再返回新創(chuàng)建的tm對象;否則直接返回已有的tm對象。
????? 需要注意的是getInstance()方法的修飾符,首先它應該是一個public方法,以便供外界其他對象使用,其次它使用了static關(guān)鍵字,即它是一個靜態(tài)方法,在類外可以直接通過類名來訪問,而無須創(chuàng)建TaskManager對象,事實上在類外也無法創(chuàng)建TaskManager對象,因為構(gòu)造函數(shù)是私有的。?
|
???????通過以上三個步驟,我們完成了一個最簡單的單例類的設(shè)計,其完整代碼如下:
[java]?view plaincopy???????在類外我們無法直接創(chuàng)建新的TaskManager對象,但可以通過代碼TaskManager.getInstance()來訪問實例對象,第一次調(diào)用getInstance()方法時將創(chuàng)建唯一實例,再次調(diào)用時將返回第一次創(chuàng)建的實例,從而確保實例對象的唯一性。
??????上述代碼也是單例模式的一種最典型實現(xiàn)方式,有了以上基礎(chǔ),理解單例模式的定義和結(jié)構(gòu)就非常容易了。單例模式定義如下:?
| 單例模式(Singleton Pattern):確保某一個類只有一個實例,而且自行實例化并向整個系統(tǒng)提供這個實例,這個類稱為單例類,它提供全局訪問的方法。單例模式是一種對象創(chuàng)建型模式。 |
????? 單例模式有三個要點:一是某個類只能有一個實例;二是它必須自行創(chuàng)建這個實例;三是它必須自行向整個系統(tǒng)提供這個實例。
?????? 單例模式是結(jié)構(gòu)最簡單的設(shè)計模式一,在它的核心結(jié)構(gòu)中只包含一個被稱為單例類的特殊類。單例模式結(jié)構(gòu)如圖3-2所示:
??????單例模式結(jié)構(gòu)圖中只包含一個單例角色:
????? ●?Singleton(單例):在單例類的內(nèi)部實現(xiàn)只生成一個實例,同時它提供一個靜態(tài)的getInstance()工廠方法,讓客戶可以訪問它的唯一實例;為了防止在外部對其實例化,將其構(gòu)造函數(shù)設(shè)計為私有;在單例類內(nèi)部定義了一個Singleton類型的靜態(tài)對象,作為外部共享的唯一實例。
3.3 負載均衡器的設(shè)計與實現(xiàn)
| ?????? Sunny軟件公司承接了一個服務(wù)器負載均衡(Load Balance)軟件的開發(fā)工作,該軟件運行在一臺負載均衡服務(wù)器上,可以將并發(fā)訪問和數(shù)據(jù)流量分發(fā)到服務(wù)器集群中的多臺設(shè)備上進行并發(fā)處理,提高系統(tǒng)的整體處理能力,縮短響應時間。由于集群中的服務(wù)器需要動態(tài)刪減,且客戶端請求需要統(tǒng)一分發(fā),因此需要確保負載均衡器的唯一性,只能有一個負載均衡器來負責服務(wù)器的管理和請求的分發(fā),否則將會帶來服務(wù)器狀態(tài)的不一致以及請求分配沖突等問題。如何確保負載均衡器的唯一性是該軟件成功的關(guān)鍵。 |
??????Sunny公司開發(fā)人員通過分析和權(quán)衡,決定使用單例模式來設(shè)計該負載均衡器,結(jié)構(gòu)圖如圖3-3所示:
在圖3-3中,將負載均衡器LoadBalancer設(shè)計為單例類,其中包含一個存儲服務(wù)器信息的集合serverList,每次在serverList中隨機選擇一臺服務(wù)器來響應客戶端的請求,實現(xiàn)代碼如下所示: [java]?view plaincopy
| 服務(wù)器負載均衡器具有唯一性! 分發(fā)請求至服務(wù)器:? Server 1 分發(fā)請求至服務(wù)器:? Server 3 分發(fā)請求至服務(wù)器:? Server 4 分發(fā)請求至服務(wù)器:? Server 2 分發(fā)請求至服務(wù)器:? Server 3 分發(fā)請求至服務(wù)器:? Server 2 分發(fā)請求至服務(wù)器:? Server 3 分發(fā)請求至服務(wù)器:? Server 4 分發(fā)請求至服務(wù)器:? Server 4 分發(fā)請求至服務(wù)器:? Server 1 |
????????雖然創(chuàng)建了四個LoadBalancer對象,但是它們實際上是同一個對象,因此,通過使用單例模式可以確保LoadBalancer對象的唯一性。
3.4 餓漢式單例與懶漢式單例的討論
??????Sunny公司開發(fā)人員使用單例模式實現(xiàn)了負載均衡器的設(shè)計,但是在實際使用中出現(xiàn)了一個非常嚴重的問題,當負載均衡器在啟動過程中用戶再次啟動該負載均衡器時,系統(tǒng)無任何異常,但當客戶端提交請求時出現(xiàn)請求分發(fā)失敗,通過仔細分析發(fā)現(xiàn)原來系統(tǒng)中還是存在多個負載均衡器對象,導致分發(fā)時目標服務(wù)器不一致,從而產(chǎn)生沖突。為什么會這樣呢?Sunny公司開發(fā)人員百思不得其解。
????? 現(xiàn)在我們對負載均衡器的實現(xiàn)代碼進行再次分析,當?shù)谝淮握{(diào)用getLoadBalancer()方法創(chuàng)建并啟動負載均衡器時,instance對象為null值,因此系統(tǒng)將執(zhí)行代碼instance= new LoadBalancer(),在此過程中,由于要對LoadBalancer進行大量初始化工作,需要一段時間來創(chuàng)建LoadBalancer對象。而在此時,如果再一次調(diào)用getLoadBalancer()方法(通常發(fā)生在多線程環(huán)境中),由于instance尚未創(chuàng)建成功,仍為null值,判斷條件(instance== null)為真值,因此代碼instance= new LoadBalancer()將再次執(zhí)行,導致最終創(chuàng)建了多個instance對象,這違背了單例模式的初衷,也導致系統(tǒng)運行發(fā)生錯誤。
????? 如何解決該問題?我們至少有兩種解決方案,在正式介紹這兩種解決方案之前,先介紹一下單例類的兩種不同實現(xiàn)方式,餓漢式單例類和懶漢式單例類。
?
1.餓漢式單例類
????? 餓漢式單例類是實現(xiàn)起來最簡單的單例類,餓漢式單例類結(jié)構(gòu)圖如圖3-4所示:
??????從圖3-4中可以看出,由于在定義靜態(tài)變量的時候?qū)嵗瘑卫?#xff0c;因此在類加載的時候就已經(jīng)創(chuàng)建了單例對象,代碼如下所示: [java]?view plaincopy
2.懶漢式單例類與線程鎖定
????? 除了餓漢式單例,還有一種經(jīng)典的懶漢式單例,也就是前面的負載均衡器LoadBalancer類的實現(xiàn)方式。懶漢式單例類結(jié)構(gòu)圖如圖3-5所示:
????從圖3-5中可以看出,懶漢式單例在第一次調(diào)用getInstance()方法時實例化,在類加載時并不自行實例化,這種技術(shù)又稱為延遲加載(Lazy Load)技術(shù),即需要的時候再加載實例,為了避免多個線程同時調(diào)用getInstance()方法,我們可以使用關(guān)鍵字synchronized,代碼如下所示: [java]?view plaincopy
????? 假如在某一瞬間線程A和線程B都在調(diào)用getInstance()方法,此時instance對象為null值,均能通過instance == null的判斷。由于實現(xiàn)了synchronized加鎖機制,線程A進入synchronized鎖定的代碼中執(zhí)行實例創(chuàng)建代碼,線程B處于排隊等待狀態(tài),必須等待線程A執(zhí)行完畢后才可以進入synchronized鎖定代碼。但當A執(zhí)行完畢時,線程B并不知道實例已經(jīng)創(chuàng)建,將繼續(xù)創(chuàng)建新的實例,導致產(chǎn)生多個單例對象,違背單例模式的設(shè)計思想,因此需要進行進一步改進,在synchronized中再進行一次(instance == null)判斷,這種方式稱為雙重檢查鎖定(Double-Check Locking)。使用雙重檢查鎖定實現(xiàn)的懶漢式單例類完整代碼如下所示:
[java]?view plaincopy?????? 需要注意的是,如果使用雙重檢查鎖定來實現(xiàn)懶漢式單例類,需要在靜態(tài)成員變量instance之前增加修飾符volatile,被volatile修飾的成員變量可以確保多個線程都能夠正確處理,且該代碼只能在JDK 1.5及以上版本中才能正確執(zhí)行。由于volatile關(guān)鍵字會屏蔽Java虛擬機所做的一些代碼優(yōu)化,可能會導致系統(tǒng)運行效率降低,因此即使使用雙重檢查鎖定來實現(xiàn)單例模式也不是一種完美的實現(xiàn)方式。?
|
?
3.餓漢式單例類與懶漢式單例類比較
????? 餓漢式單例類在類被加載時就將自己實例化,它的優(yōu)點在于無須考慮多線程訪問問題,可以確保實例的唯一性;從調(diào)用速度和反應時間角度來講,由于單例對象一開始就得以創(chuàng)建,因此要優(yōu)于懶漢式單例。但是無論系統(tǒng)在運行時是否需要使用該單例對象,由于在類加載時該對象就需要創(chuàng)建,因此從資源利用效率角度來講,餓漢式單例不及懶漢式單例,而且在系統(tǒng)加載時由于需要創(chuàng)建餓漢式單例對象,加載時間可能會比較長。
????? 懶漢式單例類在第一次使用時創(chuàng)建,無須一直占用系統(tǒng)資源,實現(xiàn)了延遲加載,但是必須處理好多個線程同時訪問的問題,特別是當單例類作為資源控制器,在實例化時必然涉及資源初始化,而資源初始化很有可能耗費大量時間,這意味著出現(xiàn)多線程同時首次引用此類的機率變得較大,需要通過雙重檢查鎖定等機制進行控制,這將導致系統(tǒng)性能受到一定影響。
3.5 一種更好的單例實現(xiàn)方法
???????餓漢式單例類不能實現(xiàn)延遲加載,不管將來用不用始終占據(jù)內(nèi)存;懶漢式單例類線程安全控制煩瑣,而且性能受影響??梢?#xff0c;無論是餓漢式單例還是懶漢式單例都存在這樣那樣的問題,有沒有一種方法,能夠?qū)煞N單例的缺點都克服,而將兩者的優(yōu)點合二為一呢?答案是:Yes!下面我們來學習這種更好的被稱之為Initialization Demand Holder (IoDH)的技術(shù)。
????? 在IoDH中,我們在單例類中增加一個靜態(tài)(static)內(nèi)部類,在該內(nèi)部類中創(chuàng)建單例對象,再將該單例對象通過getInstance()方法返回給外部使用,實現(xiàn)代碼如下所示:
[java]?view plaincopy???????編譯并運行上述代碼,運行結(jié)果為:true,即創(chuàng)建的單例對象s1和s2為同一對象。由于靜態(tài)單例對象沒有作為Singleton的成員變量直接實例化,因此類加載時不會實例化Singleton,第一次調(diào)用getInstance()時將加載內(nèi)部類HolderClass,在該內(nèi)部類中定義了一個static類型的變量instance,此時會首先初始化這個成員變量,由Java虛擬機來保證其線程安全性,確保該成員變量只能初始化一次。由于getInstance()方法沒有任何線程鎖定,因此其性能不會造成任何影響。
??????通過使用IoDH,我們既可以實現(xiàn)延遲加載,又可以保證線程安全,不影響系統(tǒng)性能,不失為一種最好的Java語言單例模式實現(xiàn)方式(其缺點是與編程語言本身的特性相關(guān),很多面向?qū)ο笳Z言不支持IoDH)。
|
??????至此,三種單例類的實現(xiàn)方式我們均已學習完畢,它們分別是餓漢式單例、懶漢式單例以及IoDH。
3.6 單例模式總結(jié)
???????單例模式作為一種目標明確、結(jié)構(gòu)簡單、理解容易的設(shè)計模式,在軟件開發(fā)中使用頻率相當高,在很多應用軟件和框架中都得以廣泛應用。
?
1.主要優(yōu)點
?????? 單例模式的主要優(yōu)點如下:
?????? (1)?單例模式提供了對唯一實例的受控訪問。因為單例類封裝了它的唯一實例,所以它可以嚴格控制客戶怎樣以及何時訪問它。
?????? (2)?由于在系統(tǒng)內(nèi)存中只存在一個對象,因此可以節(jié)約系統(tǒng)資源,對于一些需要頻繁創(chuàng)建和銷毀的對象單例模式無疑可以提高系統(tǒng)的性能。
?????? (3)?允許可變數(shù)目的實例。基于單例模式我們可以進行擴展,使用與單例控制相似的方法來獲得指定個數(shù)的對象實例,既節(jié)省系統(tǒng)資源,又解決了單例單例對象共享過多有損性能的問題。
?
2.主要缺點
?????? 單例模式的主要缺點如下:
?????? (1)?由于單例模式中沒有抽象層,因此單例類的擴展有很大的困難。
?????? (2)?單例類的職責過重,在一定程度上違背了“單一職責原則”。因為單例類既充當了工廠角色,提供了工廠方法,同時又充當了產(chǎn)品角色,包含一些業(yè)務(wù)方法,將產(chǎn)品的創(chuàng)建和產(chǎn)品的本身的功能融合到一起。
?????? (3)?現(xiàn)在很多面向?qū)ο笳Z言(如Java、C#)的運行環(huán)境都提供了自動垃圾回收的技術(shù),因此,如果實例化的共享對象長時間不被利用,系統(tǒng)會認為它是垃圾,會自動銷毀并回收資源,下次利用時又將重新實例化,這將導致共享的單例對象狀態(tài)的丟失。
?
3.適用場景
?????? 在以下情況下可以考慮使用單例模式:
?????? (1)?系統(tǒng)只需要一個實例對象,如系統(tǒng)要求提供一個唯一的序列號生成器或資源管理器,或者需要考慮資源消耗太大而只允許創(chuàng)建一個對象。
?????? (2)?客戶調(diào)用類的單個實例只允許使用一個公共訪問點,除了該公共訪問點,不能通過其他途徑訪問該實例。
?
|
總結(jié)
以上是生活随笔為你收集整理的确保对象的唯一性——单例模式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 请求的链式处理——职责链模式
- 下一篇: 合成复用原则