Singleton設計模式

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

本文會帶着你深刻整個Singleton的世界,固然,我會放棄使用C++語言而改用Java語言,由於使用Java這個語言可能更容易讓我說明一些事情。程序員

Singleton的教學版本面試

這裏,我將直接給出一個Singleton的簡單實現,由於我相信你已經有這方面的一些基礎了。咱們姑且把這個版本叫作1.0版 1 2 3 4 5 6 7 8 9 10 11shell

// version 1.0 public class Singleton { private static Singleton singleton = null; private Singleton() { } public static Singleton getInstance() { if (singleton== null) { singleton= new Singleton(); } return singleton; } }bootstrap

在上面的實例中,我想說明下面幾個Singleton的特色:(下面這些東西多是盡人皆知的,沒有什麼新鮮的)設計模式

私有(private)的構造函數,代表這個類是不可能造成實例了。這主要是怕這個類會有多個實例。
即然這個類是不可能造成實例,那麼,咱們須要一個靜態的方式讓其造成實例:getInstance()。注意這個方法是在new本身,由於其能夠訪問私有的構造函數,因此他是能夠保證明例被建立出來的。
在getInstance()中,先作判斷是否已造成實例,若是已造成則直接返回,不然建立實例。
所造成的實例保存在本身類中的私有成員中。
咱們取實例時,只須要使用Singleton.getInstance()就好了。
複製代碼

固然,若是你以爲知道了上面這些事情後就學成了,那得給你當頭棒喝一下了,事情遠遠沒有那麼簡單。 Singleton的實際版本安全

上面的這個程序存在比較嚴重的問題,由於是全局性的實例,因此,在多線程狀況下,全部的全局共享的東西都會變得很是的危險,這個也同樣,在多線程狀況下,若是多個線程同時調用getInstance()的話,那麼,可能會有多個進程同時經過 (singleton== null)的條件檢查,因而,多個實例就建立出來,而且極可能形成內存泄露問題。嗯,熟悉多線程的你必定會說——「咱們須要線程互斥或同步」,沒錯,咱們須要這個事情,因而咱們的Singleton升級成1.1版,以下所示: 1 2 3 4 5 6 7 8 9 10 11 12 13 14bash

// version 1.1 public class Singleton { private static 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版本,以下所示: 1 2 3 4 5 6 7 8 9 10 11 12 13 14函數

// version 1.2 public class Singleton { private static 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。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

// version 1.3 public class Singleton { private static 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 = new Singleton()這句,這並不是是一個原子操做,事實上在 JVM 中這句話大概作了下面 3 件事情。

給 singleton 分配內存
調用 Singleton 的構造函數來初始化成員變量,造成實例
將singleton對象指向分配的內存空間(執行完這步 singleton纔是非 null 了)
複製代碼

可是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序多是 1-2-3 也多是 1-3-2。若是是後者,則在 3 執行完畢、2 未執行以前,被線程二搶佔了,這時 instance 已是非 null 了(但卻沒有初始化),因此線程二會直接返回 instance,而後使用,而後瓜熟蒂落地報錯。

對此,咱們只須要把singleton聲明成 volatile 就能夠了。下面是1.4版:

// version 1.4
public class Singleton
{
    private volatile static Singleton singleton = null;
    private Singleton()  {    }
    public static Singleton getInstance()   {
        if (singleton== null)  {
            synchronized (Singleton.class) {
                if (singleton== null)  {
                    singleton= new Singleton();
                }
            }
        }
        return singleton;
    }
}
複製代碼

使用 volatile 有兩個功用:

1)這個變量不會在多個線程中存在複本,直接從內存讀取。

2)這個關鍵字會禁止指令重排序優化。也就是說,在 volatile 變量的賦值操做後面會有一個內存屏障(生成的彙編代碼上),讀操做不會被重排序到內存屏障以前。

可是,這個事情僅在Java 1.5版後有用,1.5版以前用這個變量也有問題,由於老版本的Java的內存模型是有缺陷的。 Singleton 的簡化版本

上面的玩法實在是太複雜了,一點也不優雅,下面是一種更爲優雅的方式:

這種方法很是簡單,由於單例的實例被聲明成 static 和 final 變量了,在第一次加載類到內存中時就會初始化,因此建立實例自己是線程安全的。

// version 1.5
public class Singleton
{
    private volatile static Singleton singleton = new Singleton();
    private Singleton()  {    }
    public static Singleton getInstance()   {
        return singleton;
    }
}
複製代碼

可是,這種玩法的最大問題是——當這個類被加載的時候,new Singleton() 這句話就會被執行,就算是getInstance()沒有被調用,類也被初始化了。

因而,這個可能會與咱們想要的行爲不同,好比,個人類的構造函數中,有一些事可能須要依賴於別的類乾的一些事(好比某個配置文件,或是某個被其它類建立的資源),咱們但願他能在我第一次getInstance()時才被真正的建立。這樣,咱們能夠控制真正的類建立的時刻,而不是把類的建立委託給了類裝載器。

好吧,咱們還得繞一下:

下面的這個1.6版是老版《Effective Java》中推薦的方式。

// version 1.6
public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
複製代碼

上面這種方式,仍然使用JVM自己機制保證了線程安全問題;因爲 SingletonHolder 是私有的,除了 getInstance() 以外沒有辦法訪問它,所以它只有在getInstance()被調用時纔會真正建立;同時讀取實例的時候不會進行同步,沒有性能缺陷;也不依賴 JDK 版本。 Singleton 優雅版本

public enum Singleton{
   INSTANCE;
}
複製代碼

竟然用枚舉!!看上去好牛逼,經過EasySingleton.INSTANCE來訪問,這比調用getInstance()方法簡單多了。

默認枚舉實例的建立是線程安全的,因此不須要擔憂線程安全的問題。可是在枚舉中的其餘任何方法的線程安全由程序員本身負責。還有防止上面的經過反射機制調用私用構造器。

這個版本基本上消除了絕大多數的問題。代碼也很是簡單,實在沒法不用。這也是新版的《Effective Java》中推薦的模式。 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 Singleton
{
    public:
        static T& Instance()
        {
            static T theSingleInstance; //假設T有一個protected默認構造函數
            return theSingleInstance;
        }
};
 
class OnlyOne : public Singleton
{
    friend class Singleton;
    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;
}
複製代碼

文章轉載自:coolshell.cn/articles/26…

相關文章
相關標籤/搜索