【設計模式】【創造型模式】單例模式

概念

單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種類型的設計模式屬於建立型模式。在 GOF 書中給出的定義爲:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。html

單例模式通常體如今類聲明中,單例的類負責建立本身的對象,同時確保只有單個對象被建立。這個類提供了一種訪問其惟一的對象的方式,能夠直接訪問,不須要實例化該類的對象。java

注意:
一、單例類只能有一個實例。
二、單例類必須本身建立本身的惟一實例。
三、單例類必須給全部其餘對象提供這一實例。編程

優缺點

優勢:
一、在內存裏只有一個實例,減小了內存的開銷,尤爲是頻繁的建立和銷燬實例(好比管理學院首頁頁面緩存)。
二、避免對資源的多重佔用(好比寫文件操做)。
缺點:
沒有接口,不能繼承,與單一職責原則衝突,一個類應該只關心內部邏輯,而不關心外面怎麼樣來實例化。設計模式

單例模式的六種寫法

單例模式的代碼要素:
一、將構造函數私有化
二、在類的內部建立實例
三、提供獲取惟一實例的方法緩存

【1】餓漢式

//code 1
public class Singleton {
    //在類內部實例化一個實例
    private static Singleton instance = new Singleton();
    //私有的構造函數,外部沒法訪問
    private Singleton() {
    }
    //對外提供獲取實例的靜態方法
    public static Singleton getInstance() {
        return instance;
    }
}

所謂餓漢。這是個比較形象的比喻。對於一個餓漢來講,他但願他想要用到這個實例的時候就可以當即拿到,而不須要任何等待時間。因此,經過static的靜態初始化方式,在該類第一次被加載的時候,就有一個SimpleSingleton的實例被建立出來了。這樣就保證在第一次想要使用該對象時,他已經被初始化好了。安全

同時,因爲該實例在類被加載的時候就建立出來了,因此也避免了線程安全問題。(緣由見:在深度分析Java的ClassLoader機制(源碼級別)Java類的加載、連接和初始化
還有一種餓漢模式的變種多線程

//code 3
public class Singleton2 {
    //在類內部定義
    private static Singleton2 instance;
    static {
        //實例化該實例
        instance = new Singleton2();
    }
    //私有的構造函數,外部沒法訪問
    private Singleton2() {
    }
    //對外提供獲取實例的靜態方法
    public static Singleton2 getInstance() {
        return instance;
    }
}

code 3code 1實際上是同樣的,都是在類被加載的時候實例化一個對象。併發

餓漢式單例,在類被加載的時候對象就會實例化。這也許會形成沒必要要的消耗,由於有可能這個實例根本就不會被用到。並且,若是這個類被屢次加載的話也會形成屢次實例化。其實解決這個問題的方式有不少,下面提供兩種解決方式,第一種是使用靜態內部類的形式。第二種是使用懶漢式。app

【2】懶漢式,線程不安全

//code 5
public class Singleton {
    //定義實例
    private static Singleton instance;
    //私有構造方法
    private Singleton(){}
    //對外提供獲取實例的靜態方法
    public static Singleton getInstance() {
        //在對象被使用的時候才實例化
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

這段代碼簡單明瞭,並且使用了懶加載模式,可是卻存在致命的問題。當有多個線程並行調用 getInstance()的時候,就會建立多個實例。也就是說在多線程下不能正常工做。函數

【3】線程安全的懶漢式

//code 6
public class SynchronizedSingleton {
    //定義實例
    private static SynchronizedSingleton instance;
    //私有構造方法
    private SynchronizedSingleton(){}
    //對外提供獲取實例的靜態方法,對該方法加鎖
    public static synchronized SynchronizedSingleton getInstance() {
        //在對象被使用的時候才實例化
        if (instance == null) {
            instance = new SynchronizedSingleton();
        }
        return instance;
    }
}

針對線程不安全的懶漢式的單例,其實解決方式很簡單,就是給建立對象的步驟加鎖。

這種寫法可以在多線程中很好的工做,並且看起來它也具有很好的延遲加載,可是,遺憾的是,他效率很低,由於99%狀況下不須要同步。(由於上面的synchronized的加鎖範圍是整個方法,該方法的全部操做都是同步進行的,可是對於非第一次建立對象的狀況,也就是沒有進入if語句中的狀況,根本不須要同步操做,能夠直接返回instance。)這就引出了雙重檢驗鎖。

【4】雙重檢測機制(DCL)懶漢式

//code 7
public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

針對上面code 6存在的問題,相信對併發編程瞭解的同窗都知道如何解決。其實上面的代碼存在的問題主要是鎖的範圍太大了。只要縮小鎖的範圍就能夠了。那麼如何縮小鎖的範圍呢?相比於同步方法,同步代碼塊的加鎖範圍更小。code 6能夠改形成上面的樣子。

雙重檢驗鎖模式Double Checked Locking Pattern),是一種使用同步塊加鎖的方法。稱其爲雙重檢查鎖,由於會有兩次檢查 instance == null,一次是在同步塊外,一次是在同步塊內。爲何在同步塊內還要再檢驗一次?由於可能會有多個線程一塊兒進入同步塊外的if,若是在同步塊內不進行二次檢驗的話就會生成多個實例了。

可是,事情這的有這麼容易嗎?上面的代碼看上去好像是沒有任何問題。實現了惰性初始化,解決了同步問題,還減少了鎖的範圍,提升了效率。可是,該代碼還存在隱患。隱患的緣由主要和Java內存模型(JMM)有關。

主要在於instance = new Singleton()這句,這並不是是一個原子操做,事實上在JVM 中這句話大概作了下面 3 件事情。

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

J2SE 1.4或更早的版本中使用雙重檢查鎖有潛在的危險,有時會正常工做(區分正確實現和有小問題的實現是很困難的。取決於編譯器,線程的調度和其餘併發系統活動,不正確的實現雙重檢查鎖致使的異常結果可能會間歇性出現。重現異常是十分困難的。) 在J2SE 5.0中,這一問題被修正了。volatile關鍵字保證多個線程能夠正確處理單件實例。

因此,針對code 7 ,能夠有code 8 和code 9兩種替代方案:

//code 8
public class VolatileSingleton {
    private static volatile VolatileSingleton singleton;

    private VolatileSingleton() {
    }

    public static VolatileSingleton getSingleton() {
        if (singleton == null) {
            synchronized (VolatileSingleton.class) {
                if (singleton == null) {
                    singleton = new VolatileSingleton();
                }
            }
        }
        return singleton;
    }
}

有些人認爲使用 volatile 的緣由是可見性,也就是能夠保證線程在本地不會存有 instance 的副本,每次都是去主內存中讀取。但實際上是不對的。使用 volatile 的主要緣由是其另外一個特性:禁止指令重排序優化。也就是說,在 volatile 變量的賦值操做後面會有一個內存屏障(生成的彙編代碼上),讀操做不會被重排序到內存屏障以前。好比上面的例子,取操做必須在執行完 1-2-3 以後或者 1-3-2 以後,不存在執行到 1-3 而後取到值的狀況。從「先行發生原則」的角度理解的話,就是對於一個 volatile 變量的寫操做都先行發生於後面對這個變量的讀操做(這裏的「後面」是時間上的前後順序)。

可是特別注意在 Java 5 之前的版本使用了 volatile 的雙檢鎖仍是有問題的。其緣由是 Java 5 之前的 JMMJava 內存模型)是存在缺陷的,即時將變量聲明成 volatile 也不能徹底避免重排序,主要是 volatile 變量先後的代碼仍然存在重排序問題。這個 volatile 屏蔽重排序的問題在 Java 5 中才得以修復,因此在這以後才能夠放心使用 volatile

上面這種雙重校驗鎖的方式用的比較普遍,他解決了前面提到的全部問題。可是,即便是這種看上去天衣無縫的方式也可能存在問題,那就是遇到序列化的時候。詳細內容後文介紹。

使用final

//code 9
class FinalWrapper<T> {
    public final T value;

    public FinalWrapper(T value) {
        this.value = value;
    }
}

public class FinalSingleton {
    private FinalWrapper<FinalSingleton> helperWrapper = null;

    public FinalSingleton getHelper() {
        FinalWrapper<FinalSingleton> wrapper = helperWrapper;

        if (wrapper == null) {
            synchronized (this) {
                if (helperWrapper == null) {
                    helperWrapper = new FinalWrapper<FinalSingleton>(new FinalSingleton());
                }
                wrapper = helperWrapper;
            }
        }
        return wrapper.value;
    }
}

【5】靜態內部類

public class Singleton {  
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    
    private Singleton (){}  
    
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE;  
    }  
}

這種方式能達到雙檢鎖方式同樣的功效,但實現更簡單。對靜態域使用延遲初始化,應使用這種方式而不是雙檢鎖方式。這種方式只適用於靜態域的狀況,雙檢鎖方式可在實例域須要延遲初始化時使用。
這種方式一樣利用了 classloder 機制來保證初始化 instance 時只有一個線程,它跟第 1 種方式不一樣的是:第 1 種方式只要 Singleton 類被裝載了,那麼 instance 就會被實例化(沒有達到 lazy loading 效果),而這種方式是 Singleton 類被裝載了,instance 不必定被初始化。由於 SingletonHolder 類沒有被主動使用,只有經過顯式調用 getInstance 方法時,纔會顯式裝載 SingletonHolder 類,從而實例化 instance。想象一下,若是實例化 instance 很消耗資源,因此想讓它延遲加載,另一方面,又不但願在 Singleton 類加載時就實例化,由於不能確保 Singleton 類還可能在其餘的地方被主動使用從而被加載,那麼這個時候實例化 instance 顯然是不合適的。這個時候,這種方式相比第 1 種方式就顯得很合理。

這種寫法仍然使用JVM自己機制保證了線程安全問題;因爲 SingletonHolder 是私有的,除了 getInstance() 以外沒有辦法訪問它,所以它是懶加載的;同時讀取實例的時候不會進行同步,沒有性能缺陷;也不依賴 JDK 版本。

【6】枚舉式

// code 10
public enum  Singleton {

    INSTANCE;
    
    Singleton() {
    }
}

Java 1.5以前,實現單例通常只有以上幾種辦法,在Java 1.5以後,還有另一種實現單例的方式,那就是使用枚舉。能夠經過Singleton.INSTANCE來訪問實例。

這種方式是Effective Java做者Josh Bloch 提倡的方式(Effective Java 第3條),它不只能避免多線程同步問題,並且還能防止反序列化從新建立新的對象(下面會介紹),可謂是很堅強的壁壘啊,在深度分析Java的枚舉類型—-枚舉的線程安全性及序列化問題中有詳細介紹枚舉的線程安全問題和序列化問題。

那這種有啥好處?枚舉的方式實現:

簡潔
無嘗提供了序列化機制
絕對防止屢次實例化,即便是在面對複雜的序列化或者反射攻擊的時候(安全)!

這種也較爲推薦使用!

具體推薦緣由以下:
深度分析Java的枚舉類型—-枚舉的線程安全性及序列化問題
爲何我牆裂建議你們使用枚舉來實現單例

注意點

有兩個問題須要注意:

一、若是單例由不一樣的類裝載器裝入,那便有可能存在多個單例類的實例。假定不是遠端存取,例如一些servlet容器對每一個servlet使用徹底不一樣的類裝載器,這樣的話若是有兩個servlet訪問一個單例類,它們就都會有各自的實例。
該問題能夠經過以下方式修復:

private static Class getClass(String classname)  
throws ClassNotFoundException {  
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    if(classLoader == null)     
          classLoader = Singleton.class.getClassLoader();     
          return (classLoader.loadClass(classname));     
       }     
    }

二、若是Singleton實現了java.io.Serializable接口,那麼這個類的實例就可能被序列化和復原。無論怎樣,若是你序列化一個單例類的對象,接下來複原多個那個對象,那你就會有多個單例類的實例。序列化問題參考下面的分析。

單例與序列化

單例與序列化的那些事兒一文中,分析過單例和序列化以前的關係——序列化能夠破壞單例。要想防止序列化對單例的破壞,只要在Singleton類中定義readResolve就能夠解決該問題。

//code 11
package com.hollis;
import java.io.Serializable;
/**
 * Created by hollis on 16/2/5.
 * 使用雙重校驗鎖方式實現單例
 */
public class Singleton implements Serializable{
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    private Object readResolve() {
        return singleton;
    }
}

總結

通常來講,單例模式有五種寫法:懶漢、餓漢、雙重檢驗鎖、靜態內部類、枚舉。上述所說都是線程安全的實現,文章給出的第2種方法不算正確的寫法。

通常狀況下直接使用餓漢式就行了,若是明確要求要懶加載(lazy initialization)會傾向於使用靜態內部類,若是涉及到反序列化建立對象時,可使用枚舉的方式來實現單例。若是有其餘特殊的需求,能夠考慮使用雙檢鎖方式

參考資料:
如何正確地寫出單例模式
單例模式的七種寫法
爲何我牆裂建議你們使用枚舉來實現單例
設計模式(二)——單例模式
單例模式
單例模式你會幾種寫法?

相關文章
相關標籤/搜索