深刻淺出單實例Singleton設計模式
陳皓
前序
單實例Singleton設計模式多是被討論和使用的最普遍的一個設計模式了,這可能也是面試中問得最多的一個設計模式了。這個設計模式主要目的是想在整個系統中只能出現一個類的實例。這樣作固然是有必然的,好比你的軟件的全局配置信息,或者是一個Factory,或是一個主控類,等等。你但願這個類在整個系統中只能出現一個實例。固然,做爲一個技術負責人的你,你固然有權利經過使用非技術的手段來達到你的目的。好比:你在團隊內部明文規定,「XX類只能有一個全局實例,若是某人使用兩次以上,那麼該人將被處於2000元的罰款!」(呵呵),你固然有權這麼作。可是若是你的設計的是東西是一個類庫,或是一個須要提供給用戶使用的API,恐怕你的這項規定將會失效。由於,你無權要求別人會那麼作。因此,這就是爲何,咱們但願經過使用技術的手段來達成這樣一個目的的緣由。
本文會帶着你深刻整個Singleton的世界,固然,我會放棄使用C++語言而改用Java語言,由於使用Java這個語言可能更容易讓我說明一些事情。
Singleton的教學版本
這裏,我將直接給出一個Singleton的簡單實現,由於我相信你已經有這方面的一些基礎了。咱們姑且把這具版本叫作1.0版
-
- public class Singleton
- {
- private static final Singleton singleton = null;
-
- private Singleton()
- {
- }
- public static Singleton getInstance()
- {
- if (singleton== null)
- {
- singleton= new Singleton();
- }
- return singleton;
- }
- }
在上面的實例中,我想說明下面幾個Singleton的特色:(下面這些東西多是盡人皆知的,沒有什麼新鮮的)
- 私有(private)的構造函數,代表這個類是不可能造成實例了。這主要是怕這個類會有多個實例。
- 即然這個類是不可能造成實例,那麼,咱們須要一個靜態的方式讓其造成實例:getInstance()。注意這個方法是在new本身,由於其能夠訪問私有的構造函數,因此他是能夠保證明例被建立出來的。
- 在getInstance()中,先作判斷是否已造成實例,若是已造成則直接返回,不然建立實例。
- 所造成的實例保存在本身類中的私有成員中。
- 咱們取實例時,只須要使用Singleton.getInstance()就好了。
固然,若是你以爲知道了上面這些事情後就學成了,那我給你當頭棒喝一下了,事情遠遠沒有那麼簡單。
Singleton的實際版本
上面的這個程序存在比較嚴重的問題,由於是全局性的實例,因此,在多線程狀況下,全部的全局共享的東西都會變得很是的危險,這個也同樣,在多線程狀況下,若是多個線程同時調用getInstance()的話,那麼,可能會有多個進程同時經過 (singleton== null)的條件檢查,因而,多個實例就建立出來,而且極可能形成內存泄露問題。嗯,熟悉多線程的你必定會說——「咱們須要線程互斥或同步」,沒錯,咱們須要這個事情,因而咱們的Singleton升級成1.1版,以下所示:
-
- public class Singleton
- {
- private static final Singleton singleton = null;
-
- private Singleton()
- {
- }
- public static Singleton getInstance()
- {
- if (singleton== null)
- {
- synchronized (Singleton.class) {
- singleton= new Singleton();
- }
- }
- return singleton;
- }
- }
嗯,使用了Java的synchronized方法,看起來不錯哦。應該沒有問題了吧?!錯!這仍是有問題!爲何呢?前面已經說過,若是有多個線程同時經過(singleton== null)的條件檢查(由於他們並行運行),雖然咱們的synchronized方法會幫助咱們同步全部的線程,讓咱們並行線程變成串行的一個一個去new,那不仍是同樣的嗎?一樣會出現不少實例。嗯,確實如此!看來,還得把那個判斷(singleton== null)條件也同步起來。因而,咱們的Singleton再次升級成1.2版本,以下所示:
-
- public class Singleton
- {
- private static final Singleton singleton = null;
-
- private Singleton()
- {
- }
- public static Singleton getInstance()
- {
- synchronized (Singleton.class)
- {
- if (singleton== null)
- {
- singleton= new Singleton();
- }
- }
- return singleton;
- }
- }
不錯不錯,看似很不錯了。在多線程下應該沒有什麼問題了,不是嗎?的確是這樣的,1.2版的Singleton在多線程下的確沒有問題了,由於咱們同步了全部的線程。只不過嘛……,什麼?!還不行?!是的,仍是有點小問題,咱們原本只是想讓new這個操做並行就能夠了,如今,只要是進入getInstance()的線程都得同步啊,注意,建立對象的動做只有一次,後面的動做全是讀取那個成員變量,這些讀取的動做不須要線程同步啊。這樣的做法感受很是極端啊,爲了一個初始化的建立動做,竟然讓咱們達上了全部的讀操做,嚴重影響後續的性能啊!
還得改!嗯,看來,在線程同步前還得加一個(singleton== null)的條件判斷,若是對象已經建立了,那麼就不須要線程的同步了。OK,下面是1.3版的Singleton。
-
- public class Singleton
- {
- private static final Singleton singleton = null;
-
- private Singleton()
- {
- }
- public static Singleton getInstance()
- {
- if (singleton== null)
- {
- synchronized (Singleton.class)
- {
- if (singleton== null)
- {
- singleton= new Singleton();
- }
- }
- }
- return singleton;
- }
- }
感受代碼開始變得有點羅嗦和複雜了,不過,這多是最不錯的一個版本了,這個版本又叫「雙重檢查」Double-Check。下面是說明:
- 第一個條件是說,若是實例建立了,那就不須要同步了,直接返回就行了。
- 否則,咱們就開始同步線程。
- 第二個條件是說,若是被同步的線程中,有一個線程建立了對象,那麼別的線程就不用再建立了。
至關不錯啊,乾得很是漂亮!請你們爲咱們的1.3版起立鼓掌!
Singleton的其它問題
怎麼?還有問題?!固然還有,請記住下面這條規則——「
不管你的代碼寫得有多好,其只能在特定的範圍內工做,超出這個範圍就要出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()方法,好比:
- public class Singleton implements Serializable
- {
- ......
- ......
- protected Object readResolve()
- {
- return getInstance();
- }
- }
其3、多個Java虛擬機。若是咱們的程序運行在多個Java的虛擬機中。什麼?多個虛擬機?這是一種什麼樣的狀況啊。嗯,這種狀況是有點極端,不過仍是可能出現,好比EJB或RMI之流的東西。要在這種環境下避免多實例,看來只能經過良好的設計或非技術來解決了。
其四,volatile變量。關於volatile這個關鍵字所聲明的變量能夠被看做是一種 「程度較輕的同步synchronized」;與 synchronized 塊相比,volatile 變量所需的編碼較少,而且運行時開銷也較少,可是它所能實現的功能也僅是synchronized的一部分。固然,如前面所述,咱們須要的Singleton只是在建立的時候線程同步,然後面的讀取則不須要同步。因此,volatile變量並不能幫助咱們即能解決問題,又有好的性能。並且,這種變量只能在JDK 1.5+版後才能使用。
其5、關於繼承。是的,繼承於Singleton後的子類也有可能形成多實例的問題。不過,由於咱們早把Singleton的構造函數聲明成了私有的,因此也就杜絕了繼承這種事情。
其六,關於代碼重用。也話咱們的系統中有不少個類須要用到這個模式,若是咱們在每個類都中有這樣的代碼,那麼就顯得有點傻了。那麼,咱們是否可使用一種方法,把這具模式抽象出去?在C++下這是很容易的,由於有模板和友元,還支持棧上分配內存,因此比較容易一些(程序以下所示),Java下可能比較複雜一些,聰明的你知道怎麼作嗎?
- template<CLASS T> class Singleton
- {
- public:
- static T& Instance()
- {
- static T theSingleInstance;
- return theSingleInstance;
- }
- };
-
- class OnlyOne : public Singleton<ONLYONE>
- {
- friend class Singleton<ONLYONE>;
- int example_data;
-
- public:
- int GetExampleData() const {return example_data;}
- protected:
- OnlyOne(): example_data(42) {}
- OnlyOne(OnlyOne&) {}
- };
-
- int main( )
- {
- cout << OnlyOne::Instance().GetExampleData()<< endl;
- return 0;
- }
本文同時發表於——酷殼:[url]http://cocre.com/?p=265[/url]
(
轉載時請註明做者和出處。未經許可,請勿用於商業用途)
(全文完)