前言
單例模式是我們所要介紹的創建型模式中的最后一種設計模式,它與我們前面介紹過的四種創建型模式有相似之處,亦有很大的不同之處。相似之處是它們都屬於創建型模式,抽象了對象類實例化的過程;而不同之處是在於單例模式在創建對象實例時,在全局范圍內保證只會創建存在該對象類的一個實例對象,同時提供其全局訪問點,而其他的四個創建型模式並沒有此限制,可以自由地創建實例化多個對象類實例,這是它們之間的最大區別。單例模式在特定的場合下,有其獨到的用處,接下來,就讓我們揭開其神秘的面紗吧!
動機
在軟件系統中,有時會出現這樣一種需求:在系統運行時刻,某一個類在全局范圍內只有一個對象實例,而且獲取該對象實例只能通過該對象類特定的訪問接口來獲取,以便繞過常規的構造器,來避免在全局運行環境中實例化多個類對象實例。面對這樣的一種需求,如何提供一種封裝機制來保證一個對象類只有一個實例?需要注意的是,客戶端使用該對象類時,是不會考慮此類是否只有一個實例存在的問題,這應該屬於類設計者的責任,由其來保證,而不是類使用者的責任。同時,由於這個全局唯一的對象實例擁有了所屬類的全部“權力”,自然它也就擔負起了行使這些權力的職責!這就是我們說的——單例模式!
意圖
保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。
結構圖
- Singleton(單例)角色:定義一個Instance操作,允許客戶訪問它的唯一實例。Instance是一個類操作,通常為靜態方法,負責創建它自己的唯一實例。
代碼示例
1: public class Singleton {
2: static Singleton instance=null;
3: private Singleton(){
4:
5: }
6: public static Singleton Instance(){
7: if(instance==null){
8: instance=new Singleton();
9: }
10: return instance;
11: }
12: //省略其他功能方法。。。
13: }
上述示例代碼是單例模式最簡單的實現,實現了單例模式的基本要求,提供一個全局訪問點來獲取得到單例類對象唯一對象實例,同時將單例類的構造函數訪問修飾符設置為private,阻止客戶程序直接通過new的方式來創建單例類的對象實例,保證對象的全局唯一性。當然,這只是示例代碼,在實際的生產環境中,我們需要根據線程安全、性能等方面來改造單例模式的實現方式,下方將會有詳細的講解。
現實場景
在我們日常的現實生活中,有很多常見的場景與我們所講的單例模式很相似。比如,每台計算機可以有很多打印機,但是只能有一個Printer Spooler,避免兩個打印任務同時輸出到打印機中;國家主席職位是一個單例模式,因為我們國家憲法規定在任何一個時刻,國家只能有一個國家主席(不包括副主席),所以不管當前主席個人身份如何,我們總是可以通過中華人民共和國主席這個職稱來獲取當前國家主席的所有信息,換句話來說,也只有通過它來行使憲法賦予它的各種權力和義務,因為它的”全局唯一性“。再比如,就是我們日常web開發常用的spring開源框架中對各種bean創建時的對象實例個數的指定,即scope=”singleton”,這個配置項的目的便是告之spring容器,該bean在運行時刻只能存在一個全局實例,也就是任何一個地方引用的都是這個唯一的實例對象,深入spring源碼,我們也不能發現,其實其內部基本上也是單例模式的實現而已。
接下來,我們着重來講述一下,對單例模式的不同實現方式及其特點吧。這也是單例模式中最有意義的部分呢,希望大家一起來學習、理解並掌握它們的不同之處。
示例代碼就是一種最簡單的單例實現方式,在單線程的環境之下,基本可以勝任,但是若是置於多線程的環境中,就面臨着線程安全問題呢。之所以這么說,是因為可能會出現多個單例對象。下面我們通過圖示的方法來說明一下示例代碼中獲取單例對象的Instance()方法是如何在運行時刻創建出兩個所謂的單例對象的,現在假設,有兩個對線程A和B,它們同時調用Instance()方法,那么實際的執行過程就有可能是這樣的:
如果按照上圖的執行順序,那么,這里A線程和B線程就都各創建了一個單例實例對象,也就違反了單例模式的本質。示例代碼中的單例模式實現方式為懶漢式,如果要求線程安全,通常有兩種方式,一個是在Instance()方法上加上sysnchronized關鍵字,即:
1: public static synchronized Singleton Instance(){
2: if(instance==null){
3: instance=new Singleton();
4: }
5: return instance;
6: }
關鍵字synchronized將會保證Instance()方法的線程安全,但是這樣一來,會降低整個訪問速度,而且每次都需要進行判斷。有沒有一種更好的方式來實現懶漢式實現方式即線程安全又能保證執行效率呢?
答案就是雙重加鎖機制,具體指的是:並不是每次進入Instance()方法都需要進行同步,而是先不同步,進入方法過后,先檢查實例是否存在,如果不存在才進入下面的同步塊,這是第一重檢票。進入同步塊后,再次檢查實例是否存在,如果不存在,就在同步的情況下創建一個單例對象實例,這是第二重檢查。這樣一來,就只需要同步一次,從而減小了同步情況下進行判斷所浪費的時間。
雙重檢查加鎖機制的實現會使用一個關鍵字volatile,它的意思是:被volatile修飾的變量的值,將不會被本地線程緩存,所有對該變量的讀寫都是直接操作共享內存,從而確保多個線程能正確的處理該變量。需要注意的是,在java1.4版本及之前版本中,很多JVM對volatile關鍵字的實現有問題,建議在java5及以上版本上使用。實現代碼如下:
1: public class Singleton {
2: private static volatile Singleton instance=null;
3: private Singleton(){
4:
5: }
6: public static Singleton Instance(){
7: if(instance==null){
8: synchronized (Singleton.class) {
9: if(instance==null){
10: instance=new Singleton();
11: }
12: }
13: }
14: return instance;
15: }
16: }
這里需要提及的一點是,由於volatile關鍵字可能會屏蔽掉虛擬機中一些必要的代碼優化,所以運行效率並不是很高。因此一般建議,沒有特別的需要,不建議使用。換句話來說,雖然使用雙重加鎖機制可以實現線程安全的單例模式,但並不建議大量使用。既然這樣子,是否有一種更加合理的方式來完成對單例模式的實現呢?
這里也有兩種選擇,一種是餓漢式實現方式,一種還是懶漢式實現方式。餓漢式實現方式比較簡單,直接在Singleton類加載的過程中,就將靜態類型的instance變量實例化,由虛擬來保證其線程安全性,唯一不足的是無法實現延遲加載,不過這種方式,通過情況下已經足夠高效呢,實現也比較簡單,示例代碼如下所示:
1: public class Singleton {
2: static Singleton instance=new Singleton();
3: private Singleton(){}
4:
5: public static Singleton Instance(){
6: return instance;
7: }
8: }
不過也有一種懶漢式的實現方方式,名日:Lazy initialization holder class模式,其綜合使用了java的類級內部類和多線程缺省同步鎖知識,很巧妙地同時實現了延遲加載和線程安全,在介紹其具體的實現之前,我們先來普及下基礎知識吧!
何謂類級內部類,指的是有static修飾的成員式內部類,如果沒有static修飾的內部類稱為對象級內部類。類級內部類相當於其外部類的static成分,它與對象與外部類對象間不存在依賴關系,因此可以直接創建。在類級內部類中,可以定義靜態的方法,在靜態方法中只能引用外部類中的靜態方法或者靜態成員變量。類級內部類相當於外部類的成員,只有在第一次被使用時才會被加載。
接下來,我們看看有哪些情況下,JVM會隱含地為我們執行同步操作,這些情況下,我們不需要自己來進行同步控制呢:
- 由靜態初始化器(在靜態字段上或者是static{}塊中的初始化器)初始化數據時。
- 訪問final字段時。
- 在創建線程之前創建對象時。
- 線程可以看見它將要處理的對象時。
有了上面兩部分知識,再來理解這種高效的單例實現方式就比較簡單呢!
1: public class Singleton {
2: //類級內部類,與外部類實例沒有綁定關系,只有在被調用時才會被加載,從而實現了延遲加載
3: private static class SingletonHolder{
4: //靜態初始化器,由JVM來保證線程安全
5: private static Singleton instance=new Singleton();
6: }
7:
8: private Singleton(){}
9:
10: public static Singleton Instance(){
11: return SingletonHolder.instance;
12: }
13: }
結合上面的基礎知識和代碼上的注釋,仔細想想,這種方法是不是很巧妙呢?在Instance()方法第一次被調用時,它第一次讀取SingletonHolder.instance,導致SingletonHolder類得到初始化;而SingletonHolder類被裝載並初始化的時候,也會初始化其靜態域,也就會創建Singleton的對象實例,因為是靜態域,因此會由在虛擬機裝載類的時候初始化一次,並由JVM來保證其線程安全性。綜上所述,該實現方法,不僅實現了延遲加載,又實現了線程安全,確實是一種值得推薦的單例實現方法,大家好好理解此方法的精妙之處吧!(來自《研磨設計模式》一書)
實現要點
- 單例模式模式用於限制對單例類實例的創建
- Singleton類的構造器可以是protected,被子類派生
- Singleton類一般不需要實現Cloneable接口,因為克隆操作可能會產生多個單例類對象,與單例模式的初衷相佐
- 單例模式關注點在單例類的實例的創建上,沒有涉及實例的銷毀管理等工作,我們可以通過使用一個單例注冊表來完成對各種單例類實例的管理工作。
運用效果
- 對象實例控制:單例模式提供全局訪問點,可以保證對客戶端都只訪問到唯一一個單例實例對象。
- 創建的方便性:由於單例類控制了實例創建,可以根據實際情況方便地修改單例對象的實例化過程。
- 由於單例模式不能通過new的方式直接創建單例對象,因此單例類的使用都必須事先知道該類為單例類,否則會因為看不到源碼,而造成類的使用性差的印象。
- 由於單例類全局只有一個對象實例,但是對其的引用卻可能不只一個,因為不能簡單地對這個特殊的對象實例進行銷毀操作,換句話來說就是不能輕易地手工地銷毀該對象。在存在內存管理的語言中,這個問題我們可以不能過多地關注這個問題,運行時會幫我們自動銷毀已經不存在引用的對象,但是對於c++語言而言,如果簡單地銷毀單例對象,有可能會造成“懸浮引用”問題。
適用性
- 當類只能有一個實例而且客戶可以從一個眾所周知的訪問點訪問它時。
- 當這個唯一實例應該是通過子類化可擴展的,並且客戶應該無需更改代碼就能使用一個擴展的實例時(這點不是很理解,理解的同學留言歡迎交流)。
相關模式
很多模式都可以使用單例模式,只要這些模式的某個類,需要控制實例為一個的時候,就可以通過單例模式來完成。比如抽象工作方法中的具體工廠類通常就可以設計成一個單例類等等。
總結
單例模式的本質是:控制實例數目。這里說的實例數目不一定就指的是一個單例實例,只是說是實例對象數量有一定限制而已。通過單例模式我們可以很容易控制單例類的創建和訪問,請記住,單例類實例的個數控制問題是類設計者應該考慮的問題,而不是類使用者考慮的問題,作為單例類的設計者,我們應該時刻記住這一原則,指導我們實現單例模式。單例模式的幾種通常的實現方法在上文中已經有比較詳細的介紹,希望大家能夠好好理解其中內含,學以致用。下一篇,我們將會對之前介紹過的所有創建型模式進行概括性地梳理和總結,敬請期待!
參考資料:
- 程傑著《大話設計模式》一書
- 陳臣等著《研磨設計模式》一書
- GOF著《設計模式》一書
- Terrylee .Net設計模式系列文章
- 呂震宇老師 設計模式系列文章