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

喝水會長肉發表於2021-12-12

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

概念

單例模式( 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 ( ) {
    }
//java學習交流:737251827  進入可領取學習資源及對十年開發經驗大佬提問,免費解答!
    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 ;
    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 ;
    }
}


總結

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

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

 



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70010294/viewspace-2847254/,如需轉載,請註明出處,否則將追究法律責任。

相關文章