設計模式(二)——單例模式

櫻桃大丸子 9年前發布 | 23K 次閱讀 設計模式

設計模式(一)——設計模式概述中簡單介紹了設計模式以及各種設計模式的基本概念,本文主要介紹單例設計模式。包括單例的概念、用途、實現方式、如何防止被序列化破壞等。

概念

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

單例模式一般體現在類聲明中,單例的類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。

用途

單例模式有以下兩個優點:

在內存里只有一個實例,減少了內存的開銷,尤其是頻繁的創建和銷毀實例(比如網站首頁頁面緩存)。

避免對資源的多重占用(比如寫文件操作)。

有時候,我們在選擇使用單例模式的時候,不僅僅考慮到其帶來的優點,還有可能是有些場景就必須要單例。比如類似”一個黨只能有一個主席”的情況。

實現方式

我們知道,一個類的對象的產生是由類構造函數來完成的。如果一個類對外提供了public的構造方法,那么外界就可以任意創建該類的對象。所以,如果想限制對象的產生,一個辦法就是將構造函數變為私有的(至少是受保護的),使外面的類不能通過引用來產生對象。同時為了保證類的可用性,就必須提供一個自己的對象以及訪問這個對象的靜態方法。

設計模式(二)——單例模式

餓漢式

下面是一個簡單的單例的實現:

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

使用以下代碼測試:

//code2
public class SingletonClient {

    public static void main(String[] args) {
        SimpleSingleton simpleSingleton1 = SimpleSingleton.getInstance();
        SimpleSingleton simpleSingleton2 = SimpleSingleton.getInstance();
        System.out.println(simpleSingleton1==simpleSingleton2);
    }
}

輸出結果:

true

code 1就是一個簡單的單例的實現,這種實現方式我們稱之為餓漢式。所謂餓漢。這是個比較形象的比喻。對于一個餓漢來說,他希望他想要用到這個實例的時候就能夠立即拿到,而不需要任何等待時間。所以,通過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 3和code 1其實是一樣的,都是在類被加載的時候實例化一個對象。

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

靜態內部類式

先來看通過靜態內部類的方式解決上面的問題:

//code 4
public class StaticInnerClassSingleton {
    //在靜態內部類中初始化實例對象
    private static class SingletonHolder {
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }
    //私有的構造方法
    private StaticInnerClassSingleton() {
    }
    //對外提供獲取實例的靜態方法
    public static final StaticInnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

這種方式同樣利用了classloder的機制來保證初始化instance時只有一個線程,它跟餓漢式不同的是(很細微的差別):餓漢式是只要Singleton類被裝載了,那么instance就會被實例化(沒有達到lazy loading效果),而這種方式是Singleton類被裝載了,instance不一定被初始化。因為SingletonHolder類沒有被主動使用,只有顯示通過調用getInstance方法時,才會顯示裝載SingletonHolder類,從而實例化instance。想象一下,如果實例化instance很消耗資源,我想讓他延遲加載,另外一方面,我不希望在Singleton類加載時就實例化,因為我不能確保Singleton類還可能在其他的地方被主動使用從而被加載,那么這個時候實例化instance顯然是不合適的。這個時候,這種方式相比餓漢式更加合理。

懶漢式

下面看另外一種在該對象真正被使用的時候才會實例化的單例模式——懶漢模式。

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

上面這種單例叫做懶漢式單例。懶漢,就是不會提前把實例創建出來,將類對自己的實例化延遲到第一次被引用的時候。getInstance方法的作用是希望該對象在第一次被使用的時候被new出來。

有沒有發現,其實code 5這種懶漢式單例其實還存在一個問題,那就是線程安全問題。在多線程情況下,有可能兩個線程同時進入if語句中,這樣,在兩個線程都從if中退出的時候就創建了兩個不一樣的對象。(這里就不詳細講解了,不理解的請惡補多線程知識)。

線程安全的懶漢式

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

//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。)

雙重校驗鎖

針對上面code 6存在的問題,相信對并發編程了解的同學都知道如何解決。其實上面的代碼存在的問題主要是鎖的范圍太大了。只要縮小鎖的范圍就可以了。那么如何縮小鎖的范圍呢?相比于同步方法,同步代碼塊的加鎖范圍更小。code 6可以改造成:

//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 7是對于code 6的一種改進寫法,通過使用同步代碼塊的方式減小了鎖的范圍。這樣可以大大提高效率。(對于已經存在singleton的情況,無須同步,直接return)。

但是,事情這的有這么容易嗎?上面的代碼看上去好像是沒有任何問題。實現了惰性初始化,解決了同步問題,還減小了鎖的范圍,提高了效率。但是,該代碼還存在隱患。隱患的原因主要和Java內存模型(JMM)有關。考慮下面的事件序列:

線程A發現變量沒有被初始化, 然后它獲取鎖并開始變量的初始化。

由于某些編程語言的語義,編譯器生成的代碼允許在線程A執行完變量的初始化之前,更新變量并將其指向部分初始化的對象。

線程B發現共享變量已經被初始化,并返回變量。由于線程B確信變量已被初始化,它沒有獲取鎖。如果在A完成初始化之前共享變量對B可見(這是由于A沒有完成初始化或者因為一些初始化的值還沒有穿過B使用的內存(緩存一致性)),程序很可能會崩潰。

(上面的例子不太能理解的同學,請惡補JAVA內存模型相關知識)

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

所以,針對code 7 ,可以有code 8 和code 9兩種替代方案:

使用volatile

//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;
    }
}

上面這種雙重校驗鎖的方式用的比較廣泛,他解決了前面提到的所有問題。但是,即使是這種看上去完美無缺的方式也可能存在問題,那就是遇到序列化的時候。詳細內容后文介紹。

使用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;
    }
}

枚舉式

在1.5之前,實現單例一般只有以上幾種辦法,在1.5之后,還有另外一種實現單例的方式,那就是使用枚舉:

// code 10
public enum  Singleton {

    INSTANCE;
    public void Singleton() {
    }
}

這種方式是Effective Java作者Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象(下面會介紹),可謂是很堅強的壁壘啊,在深度分析Java的枚舉類型—-枚舉的線程安全性及序列化問題中有詳細介紹枚舉的線程安全問題和序列化問題,不過,個人認為由于1.5中才加入enum特性,用這種方式寫不免讓人感覺生疏,在實際工作中,我也很少看見有人這么寫過,但是不代表他不好。

單例與序列化

單例與序列化的那些事兒一文中,Hollis就分析過單例和序列化之前的關系——序列化可以破壞單例。要想防止序列化對單例的破壞,只要在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;
    }
}

總結

本文中介紹了幾種實現單例的方法,主要包括餓漢、懶漢、使用靜態內部類、雙重校驗鎖、枚舉等。還介紹了如何防止序列化破壞類的單例性。

從單例的實現中,我們可以發現,一個簡單的單例模式就能涉及到這么多知識。在不斷完善的過程中可以了解并運用到更多的知識。所謂學無止境。

文中所有代碼見GitHub

參考資料

單例模式的七種寫法

雙重檢查鎖定模式

深入淺出設計模式

單例模式

來源:http://www.hollischuang.com/archives/1373

 本文由用戶 櫻桃大丸子 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!