深入淺出單實例Singleton設計模式

火星人 @ 2014-03-10 , reply:0


單實例Singleton設計模式可能是被討論和使用的最廣泛的一個設計模式了,這可能也是面試中問得最多的一個設計模式了.這個設計模式主要目的是想在整個系統中只能出現一個類的實例.這樣做當然是有必然的,比如你的軟體的全局配置信息,或者是一個Factory,或是一個主控類,等等.你希望這個類在整個系統中只能出現一個實例.當然,作為一個技術負責人的你,你當然有權利通過使用非技術的手段來達到你的目的.比如:你在團隊內部明文規定,「XX類只能有一個全局實例,如果某人使用兩次以上,那麼該人將被處於2000元的罰款!」(呵呵),你當然有權這麼做.但是如果你的設計的是東西是一個類庫,或是一個需要提供給用戶使用的API,恐怕你的這項規定將會失效.因為,你無權要求別人會那麼做.,這就是為什麼,我們希望通過使用技術的手段來達成這樣一個目的的原因.

本文會帶著你深入整個Singleton的世界,當然,我會放棄使用C 語言而改用Java語言,因為使用Java這個語言可能更容易讓我說明一些事情.

Singleton的教學版本

這裡,我將直接給出一個Singleton的簡單實現,因為我相信你已經有這方面的一些基礎了.我們姑且把這具版本叫做1.0版

// version 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版,如下所示:

// version 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版本,如下所示:

// version 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.

// version 1.3
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的環境下,嘿嘿,還是讓我們來看看下面的一些反例和一些別的事情的討論(當然,有些反例可能屬於鑽牛角尖,可能有點學院派,不過也不排除其實際可能性,就算是提個醒吧):

其一、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」.

其二、序例化.如果我們的這個Singleton類是一個關於我們程序配置信息的類.我們需要它有序列化的功能,那麼,當反序列化的時候,我們將無法控制別人不多次反序列化.不過,我們可以利用一下Serializable介面的readResolve()方法,比如:

public class Singleton implements Serializable
{
......
......
protected Object readResolve()
{
return getInstance();
}
}

其三、多個Java虛擬機.如果我們的程序運行在多個Java的虛擬機中.什麼?多個虛擬機?這是一種什麼樣的情況啊.嗯,這種情況是有點極端,不過還是可能出現,比如EJB或RMI之流的東西.要在這種環境下避免多實例,看來只能通過良好的設計或非技術來解決了.

其四,volatile變數.關於volatile這個關鍵字所聲明的變數可以被看作是一種 「程度較輕的同步synchronized」;與 synchronized 塊相比,volatile 變數所需的編碼較少,並且運行時開銷也較少,但是它所能實現的功能也僅是synchronized的一部分.當然,如前面所述,我們需要的Singleton只是在創建的時候線程同步,而後面的讀取則不需要同步.,volatile變數並不能幫助我們即能解決問題,又有好的性能.,這種變數只能在JDK 1.5 版后才能使用.

其五、關於繼承.是的,繼承於Singleton后的子類也有可能造成多實例的問題.不過,因為我們早把Singleton的構造函數聲明成了私有的,也就杜絕了繼承這種事情.

其六,關於代碼重用.也話我們的系統中有很多個類需要用到這個模式,如果我們在每一個類都中有這樣的代碼,那麼就顯得有點傻了.那麼,我們是否可以使用一種方法,把這具模式抽象出去?在C 下這是很容易的,因為有模板和友元,還支持棧上分配內存,比較容易一些(程序如下所示),Java下可能比較複雜一些,聰明的你知道怎麼做嗎?

template<CLASS T> class Singleton
{
public:
static T& Instance()
{
static T theSingleInstance; //假設T有一個protected默認構造函數
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;
}




[火星人 via ] 深入淺出單實例Singleton設計模式已經有235次圍觀

http://www.coctec.com/docs/java/show-post-62206.html