單實例Singleton設計模式多是被討論和使用的最普遍的一個設計模式了,這可能也是面試中問得最多的一個設計模式了。這個設計模式主要目的是想在整個系統中只能出現一個類的實例。這樣作固然是有必然的,好比你的軟件的全局配置信息,或者是一個Factory,或是一個主控類,等等。你但願這個類在整個系統中只能出現一個實例。固然,做爲一個技術負責人的你,你固然有權利經過使用非技術的手段來達到你的目的。好比:你在團隊內部明文規定,「XX類只能有一個全局實例,若是某人使用兩次以上,那麼該人將被處於2000元的罰款!」(呵呵),你固然有權這麼作。可是若是你的設計的是東西是一個類庫,或是一個須要提供給用戶使用的API,恐怕你的這項規定將會失效。由於,你無權要求別人會那麼作。因此,這就是爲何,咱們但願經過使用技術的手段來達成這樣一個目的的緣由。java
本文會帶着你深刻整個Singleton的世界,固然,我會放棄使用C++語言而改用Java語言,由於使用Java這個語言可能更容易讓我說明一些事情程序員
這裏,我將直接給出一個Singleton的簡單實現,由於我相信你已經有這方面的一些基礎了。咱們姑且把這個版本叫作1.0版面試
1 // version 1.0 2 public class Singleton { 3 private static Singleton singleton = null; 4 private Singleton() { } 5 public static Singleton getInstance() { 6 if (singleton== null) { 7 singleton= new Singleton(); 8 } 9 return singleton; 10 } 11 }
在上面的實例中,我想說明下面幾個Singleton的特色:(下面這些東西多是盡人皆知的,沒有什麼新鮮的)bootstrap
固然,若是你以爲知道了上面這些事情後就學成了,那得給你當頭棒喝一下了,事情遠遠沒有那麼簡單。設計模式
上面的這個程序存在比較嚴重的問題,由於是全局性的實例,因此,在多線程狀況下,全部的全局共享的東西都會變得很是的危險,這個也同樣,在多線程狀況下,若是多個線程同時調用getInstance()的話,那麼,可能會有多個進程同時經過 (singleton== null)的條件檢查,因而,多個實例就建立出來,而且極可能形成內存泄露問題。嗯,熟悉多線程的你必定會說——「咱們須要線程互斥或同步」,沒錯,咱們須要這個事情,因而咱們的Singleton升級成1.1版,以下所示:安全
1 // version 1.1 2 public class Singleton 3 { 4 private static Singleton singleton = null; 5 private Singleton() { } 6 public static Singleton getInstance() { 7 if (singleton== null) { 8 synchronized (Singleton.class) { 9 singleton= new Singleton(); 10 } 11 } 12 return singleton; 13 } 14 }
嗯,使用了Java的synchronized方法,看起來不錯哦。應該沒有問題了吧?!錯!這仍是有問題!爲何呢?前面已經說過,若是有多個線程同時經過(singleton== null)的條件檢查(由於他們並行運行),雖然咱們的synchronized方法會幫助咱們同步全部的線程,讓咱們並行線程變成串行的一個一個去new,那不仍是同樣的嗎?一樣會出現不少實例。嗯,確實如此!看來,還得把那個判斷(singleton== null)條件也同步起來。因而,咱們的Singleton再次升級成1.2版本,以下所示多線程
1 // version 1.2 2 public class Singleton 3 { 4 private static Singleton singleton = null; 5 private Singleton() { } 6 public static Singleton getInstance() { 7 synchronized (Singleton.class) { 8 if (singleton== null) { 9 singleton= new Singleton(); 10 } 11 } 12 return singleton; 13 } 14 }
不錯不錯,看似很不錯了。在多線程下應該沒有什麼問題了,不是嗎?的確是這樣的,1.2版的Singleton在多線程下的確沒有問題了,由於咱們同步了全部的線程。只不過嘛……,什麼?!還不行?!是的,仍是有點小問題,咱們原本只是想讓new這個操做並行就能夠了,如今,只要是進入getInstance()的線程都得同步啊,注意,建立對象的動做只有一次,後面的動做全是讀取那個成員變量,這些讀取的動做不須要線程同步啊。這樣的做法感受很是極端啊,爲了一個初始化的建立動做,竟然讓咱們達上了全部的讀操做,嚴重影響後續的性能啊!ide
還得改!嗯,看來,在線程同步前還得加一個(singleton== null)的條件判斷,若是對象已經建立了,那麼就不須要線程的同步了。OK,下面是1.3版的Singleton。函數
1 // version 1.3 2 public class Singleton 3 { 4 private static Singleton singleton = null; 5 private Singleton() { } 6 public static Singleton getInstance() { 7 if (singleton== null) { 8 synchronized (Singleton.class) { 9 if (singleton== null) { 10 singleton= new Singleton(); 11 } 12 } 13 } 14 return singleton; 15 } 16 }
感受代碼開始變得有點羅嗦和複雜了,不過,這多是最不錯的一個版本了,這個版本又叫「雙重檢查」Double-Check。下面是說明:性能
至關不錯啊,乾得很是漂亮!請你們爲咱們的1.3版起立鼓掌!
可是,若是你認爲這個版本大攻告成,你就錯了。
主要在於singleton = new Singleton()
這句,這並不是是一個原子操做,事實上在 JVM 中這句話大概作了下面 3 件事情。
可是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序多是 1-2-3 也多是 1-3-2。若是是後者,則在 3 執行完畢、2 未執行以前,被線程二搶佔了,這時 instance 已是非 null 了(但卻沒有初始化),因此線程二會直接返回 instance,而後使用,而後瓜熟蒂落地報錯。
對此,咱們只須要把singleton聲明成 volatile 就能夠了。下面是1.4版:
1 // version 1.4 2 public class Singleton 3 { 4 private volatile static Singleton singleton = null; 5 private Singleton() { } 6 public static Singleton getInstance() { 7 if (singleton== null) { 8 synchronized (Singleton.class) { 9 if (singleton== null) { 10 singleton= new Singleton(); 11 } 12 } 13 } 14 return singleton; 15 } 16 }
使用 volatile 有兩個功用:
1)這個變量不會在多個線程中存在複本,直接從內存讀取。
2)這個關鍵字會禁止指令重排序優化。也就是說,在 volatile 變量的賦值操做後面會有一個內存屏障(生成的彙編代碼上),讀操做不會被重排序到內存屏障以前。
可是,這個事情僅在Java 1.5版後有用,1.5版以前用這個變量也有問題,由於老版本的Java的內存模型是有缺陷的。
上面的玩法實在是太複雜了,一點也不優雅,下面是一種更爲優雅的方式:
這種方法很是簡單,由於單例的實例被聲明成 static 和 final 變量了,在第一次加載類到內存中時就會初始化,因此建立實例自己是線程安全的
1 // version 1.5 2 public class Singleton 3 { 4 private volatile static Singleton singleton = new Singleton(); 5 private Singleton() { } 6 public static Singleton getInstance() { 7 return singleton; 8 } 9 }
可是,這種玩法的最大問題是——當這個類被加載的時候,new Singleton() 這句話就會被執行,就算是getInstance()沒有被調用,類也被初始化了。
因而,這個可能會與咱們想要的行爲不同,好比,個人類的構造函數中,有一些事可能須要依賴於別的類乾的一些事(好比某個配置文件,或是某個被其它類建立的資源),咱們但願他能在我第一次getInstance()時才被真正的建立。這樣,咱們能夠控制真正的類建立的時刻,而不是把類的建立委託給了類裝載器。
好吧,咱們還得繞一下:
下面的這個1.6版是老版《Effective Java》中推薦的方式。
1 // version 1.6 2 public class Singleton { 3 private static class SingletonHolder { 4 private static final Singleton INSTANCE = new Singleton(); 5 } 6 private Singleton (){} 7 public static final Singleton getInstance() { 8 return SingletonHolder.INSTANCE; 9 } 10 }
上面這種方式,仍然使用JVM自己機制保證了線程安全問題;因爲 SingletonHolder 是私有的,除了 getInstance() 以外沒有辦法訪問它,所以它只有在getInstance()被調用時纔會真正建立;同時讀取實例的時候不會進行同步,沒有性能缺陷;也不依賴 JDK 版本。
1 public enum Singleton{ 2 INSTANCE; 3 }
竟然用枚舉!!看上去好牛逼,經過EasySingleton.INSTANCE來訪問,這比調用getInstance()方法簡單多了。
默認枚舉實例的建立是線程安全的,因此不須要擔憂線程安全的問題。可是在枚舉中的其餘任何方法的線程安全由程序員本身負責。還有防止上面的經過反射機制調用私用構造器。
這個版本基本上消除了絕大多數的問題。代碼也很是簡單,實在沒法不用。這也是新版的《Effective Java》中推薦的模式。
怎麼?還有問題?!固然還有,請記住下面這條規則——「不管你的代碼寫得有多好,其只能在特定的範圍內工做,超出這個範圍就要出Bug了」,這是「陳式第必定理」,呵呵。你能想想還有什麼狀況會讓這個咱們上面的代碼出問題嗎?
在C++下,我不是很好舉例,可是在Java的環境下,嘿嘿,仍是讓咱們來看看下面的一些反例和一些別的事情的討論(固然,有些反例可能屬於鑽牛角尖,可能有點學院派,不過也不排除其實際可能性,就算是提個醒吧):
其1、Class Loader。不知道你對Java的Class Loader熟悉嗎?「類裝載器」?!C++可沒有這個東西啊。這是Java動態性的核心。顧名思義,類裝載器是用來把類(class)裝載進JVM的。JVM規範定義了兩種類型的類裝載器:啓動內裝載器(bootstrap)和用戶自定義裝載器(user-defined class loader)。 在一個JVM中可能存在多個ClassLoader,每一個ClassLoader擁有本身的NameSpace。一個ClassLoader只能擁有一個class對象類型的實例,可是不一樣的ClassLoader可能擁有相同的class對象實例,這時可能產生致命的問題。如ClassLoaderA,裝載了類A的類型實例A1,而ClassLoaderB,也裝載了類A的對象實例A2。邏輯上講A1=A2,可是因爲A1和A2來自於不一樣的ClassLoader,它們其實是徹底不一樣的,若是A中定義了一個靜態變量c,則c在不一樣的ClassLoader中的值是不一樣的。
因而,若是我們的Singleton 1.3版本若是面對着多個Class Loader會怎麼樣?呵呵,多個實例一樣會被多個Class Loader建立出來,固然,這個有點牽強,不過他確實存在。難道咱們還要整出個1.4版嗎?但是,咱們怎麼可能在個人Singleton類中操做Class Loader啊?是的,你根本不可能。在這種狀況下,你能作的只有是——「保證多個Class Loader不會裝載同一個Singleton」。
其2、序例化。若是咱們的這個Singleton類是一個關於咱們程序配置信息的類。咱們須要它有序列化的功能,那麼,當反序列化的時候,咱們將沒法控制別人很少次反序列化。不過,咱們能夠利用一下Serializable接口的readResolve()方法,好比:
1 public class Singleton implements Serializable 2 { 3 ...... 4 ...... 5 protected Object readResolve() 6 { 7 return getInstance(); 8 } 9 }
其3、多個Java虛擬機。若是咱們的程序運行在多個Java的虛擬機中。什麼?多個虛擬機?這是一種什麼樣的狀況啊。嗯,這種狀況是有點極端,不過仍是可能出現,好比EJB或RMI之流的東西。要在這種環境下避免多實例,看來只能經過良好的設計或非技術來解決了。
其四,volatile變量。關於volatile這個關鍵字所聲明的變量能夠被看做是一種 「程度較輕的同步synchronized」;與 synchronized 塊相比,volatile 變量所需的編碼較少,而且運行時開銷也較少,可是它所能實現的功能也僅是synchronized的一部分。固然,如前面所述,咱們須要的Singleton只是在建立的時候線程同步,然後面的讀取則不須要同步。因此,volatile變量並不能幫助咱們即能解決問題,又有好的性能。並且,這種變量只能在JDK 1.5+版後才能使用。
其5、關於繼承。是的,繼承於Singleton後的子類也有可能形成多實例的問題。不過,由於咱們早把Singleton的構造函數聲明成了私有的,因此也就杜絕了繼承這種事情。
其六,關於代碼重用。也話咱們的系統中有不少個類須要用到這個模式,若是咱們在每個類都中有這樣的代碼,那麼就顯得有點傻了。那麼,咱們是否可使用一種方法,把這具模式抽象出去?在C++下這是很容易的,由於有模板和友元,還支持棧上分配內存,因此比較容易一些(程序以下所示),Java下可能比較複雜一些,聰明的你知道怎麼作嗎?
1 template class Singleton 2 { 3 public: 4 static T& Instance() 5 { 6 static T theSingleInstance; //假設T有一個protected默認構造函數 7 return theSingleInstance; 8 } 9 }; 10 11 class OnlyOne : public Singleton 12 { 13 friend class Singleton; 14 int example_data; 15 16 public: 17 int GetExampleData() const {return example_data;} 18 protected: 19 OnlyOne(): example_data(42) {} // 默認構造函數 20 OnlyOne(OnlyOne&) {} 21 }; 22 23 int main( ) 24 { 25 cout << OnlyOne::Instance().GetExampleData() << endl; 26 return 0; 27 }