Java設計模式-單例模式

爲何要使用單例模式

實際開發中,爲了節約系統資源,有時須要確保系統中某個類只有惟一的一個實例,當這個惟一實例建立成功後,就沒法再建立一個同類型的其它對象,全部的操做都只能基於這個惟一實例。爲了確保實例的惟一性,能夠經過單例模式實現。java

最簡單的單例類設計

public class Single {

    // 設置instance爲靜態變量
    private static Single instance = null;

    // 構造方法私有化
    private Single() {}

    // 靜態方法-實例化對象
    public static Single getInstance() {
        if (instance == null) {
            // 實例化對象
            instance = new Single();
        }
        return instance;
    }

    public static void main(String[] args) {
        // 經過getInstance()方法獲取實例
        Single instance1 = Single.getInstance();
        // 經過getInstance()方法獲取實例
        Single instance2 = Single.getInstance();
        // 判斷兩個對象是否相同,相同則表示獲取的是同一個實例,不然不是
        System.out.println(instance1 == instance2);
    }
}

由於構造函數的私有化,因此在類外沒法直接建立新的 Single 對象,可是能夠經過 Single.getInstance() 方法來獲取實例對象。第一次調用 getInstance() 方法時將建立一個惟一實例,再次調用的時候會返回這個實例,從而確保實例對象的惟一性。安全

思考多線程

爲何 instance 要設置成靜態變量?併發

餓漢式和懶漢式

餓漢式單例類
public class EagerSingle {

    // 定義靜態變量instance,在類加載的時候已經將其實例化,只能在類內部調用
    private static EagerSingle instance = new EagerSingle();

    // 構造函數私有化-只能從類內部訪問
    private EagerSingle() {}

    // 定義靜態方法-獲取instance對象,供類外部訪問調用
    public static EagerSingle getInstance() {
        return instance;
    }
}

在類加載時,調用 new EagerInstance() 方法將靜態變量 instance 初始化,此時類的私有構造方法會被調用,單例類的惟一實例被建立。函數

懶漢式單例類
public class LazySingle {

    // 設置instance爲靜態變量
    private static LazySingle instance = null;

    // 構造函數私有化
    private LazySingle() {}

    // 靜態方法-實例化對象
    public static LazySingle getInstance() {
        if (instance == null) {
            // 實例化instance對象
            instance = new LazySingle();
        }
        return instance;
    }
}

懶漢式單例類在第一次調用 getInstance() 方法時建立惟一實例,與餓漢式單例類不一樣,在類加載的時候並不會進行類的實例化操做,這種技術也成爲延遲加載技術,即須要的時候纔會加載實例。可是爲了不在多線程環境下多個線程同時調用 getInstance() 方法形成建立多個實例問題,可使用 sychronized 關鍵字,以下代碼:高併發

public class LazySingle {

    // 設置instance爲靜態變量
    private static LazySingle instance = null;

    // 構造函數私有化
    private LazySingle() {}

    // 靜態方法-實例化對象,使用sychronized關鍵字,保證同一時刻只可一個線程使用getInstance()方法
    public synchronized static LazySingle getInstance() {
        if (instance == null) {
            // 實例化instance對象
            instance = new LazySingle();
        }
        return instance;
    }
}

該單例類在 getInstance() 方法上使用 sychronized 關鍵字,保證多線程下同時訪問 getInstance() 方法的問題。可是每次調用 getInstance() 方法都須要進行線程鎖定判斷,高併發下會致使系統性能會大大下降。如何解決呢?由於單例類的目標是保證有且只有一個實例,因此咱們只要對 instance = new LazySingle() 代碼加鎖便可,無需對整個 getInstance() 方法加鎖。所以能夠對以上代碼進行修改,以下代碼:性能

public class LazySingle {

    // 設置instance爲靜態變量
    private volatile static LazySingle instance = null;

    // 構造函數私有化
    private LazySingle() {}

    // 靜態方法-實例化對象,使用sychronized關鍵字,保證同一時刻只可一個線程使用getInstance()方法
    public static LazySingle getInstance() {
        if (instance == null) {
            synchronized (LazySingle.class) {
                instance = new LazySingle();
            }
        }
        return instance;
    }
}

可是該代碼仍是存在缺陷,多線程環境下可能仍是建立了多個對象,緣由以下:線程

當線程 T1 和線程 T2 在同時調用 getInstance() 方法,此時 instance 爲 null,因此均可以經過 instance == null 判斷,可是由於 instance = new LazySingle() 代碼進行了加鎖,因此同時只可讓一個線程進入,若是線程 T1 拿到了鎖,則會建立一個實例。此時線程 T2 處於阻塞狀態,等待線程 T1 釋放鎖,線程 T1 執行完畢後釋放鎖,線程 T2 拿到鎖後一樣會執行 instance = new LazySingle() 代碼建立一個實例,這樣就致使了整個單例類建立了兩個實例的問題。因此須要對代碼進行改進,能夠在 instance = new LazySingle() 代碼前再進行一個判斷,使用 if (instance == null) 語句,這樣當線程 T2 拿到鎖後會判斷是否建立了實例,若是建立則不會再建立了,代碼以下:設計

public class LazySingle {

    // 設置instance爲靜態變量,使用volatile關鍵字保證instance共享變量的內存可見性
    private volatile static LazySingle instance = null;

    // 構造函數私有化
    private LazySingle() {}

    // 靜態方法-實例化對象,使用sychronized關鍵字,保證同一時刻只可一個線程使用getInstance()方法
    public static LazySingle getInstance() {
        if (instance == null) {
            synchronized (LazySingle.class) {
                if (instance == null) {
                    instance = new LazySingle();
                }
            }
        }
        return instance;
    }
}

須要注意,使用該方式須要使用 volatile 關鍵字修改 instance 變量,這樣能夠保證線程 T1 對 instance 變量的修改對線程 T2 當即可見,不然線程 T2 執行 instance == null 是爲 true 的。code

餓漢式與懶漢式比較

餓漢式單例類:在類加載的時候就將本身實例化,它的優勢在於無需考慮多線程環境下的訪問問題,能夠很好的確保實例的惟一性,從調用速度和反應時間上看,餓漢式單例類會優於懶漢式單例類,由於實例一開始就被建立了。可是不管系統在運行的時候是否須要該對象都會在類加載的時候建立,所以從資源利用效率看餓漢式單例類不及懶漢式單例類。並且系統加載的時候就要建立對象,因此加載時間會比懶漢式要長。

懶漢式單例類:在第一次調用時建立實例,無需一直佔用系統資源,實現了延遲加載,可是必需要處理多線程的訪問問題,即多個線程同時調用建立實例的方法可能致使建立了多個實例的問題。因此須要使用鎖機制來控制,這樣會影響系統性能。

更好的實現方法

餓漢式單例類會在類加載的時候建立實例,不能實現延遲加載,因此無論用不用單例都會一直佔據內存。懶漢式單例類使用鎖機制保證線程安全,影響系統性能。那麼如何將兩者優勢結合並克服缺點?使用 Initialization Demand Holder (IoDH) 技術。代碼以下:

public class BestSingle {

    // 構造函數私有化
    private BestSingle() {}

    // 定義一個靜態內部類,其中建立一個BestSingle實例
    public static class HolderClass {
        private static BestSingle instance = new BestSingle();
    }

    // 靜態方法,返回instance對象
    public static BestSingle getInstance() {
        return HolderClass.instance;
    }

    public static void main(String[] args) {
        BestSingle instance1, instance2;
        instance1 = BestSingle.getInstance();
        instance2 = BestSingle.getInstance();
        System.out.println(instance1 == instance2);
    }
}

在單例類中加入一個靜態內部類,在內部類中建立單例,再將該單例對象經過 getInstance() 方法返回給外部使用。因爲靜態單例對象並無做爲 BestSingle 的成員變量直接實例化,由於類加載的時候不會建立實例,第一次調用 getInstance() 方法會加載靜態內部類 HolderClass,在該類內部定義了一個靜態變量 intance,此時會初始化這個變量,因爲 getInstance() 方法沒有使用任何鎖機制,對性能不會形成任何影響。因此經過這種方式便可以實現延遲加載,而且在保證線程安全的基礎上不影響系統性能。因此該方法是最好的單例模式實現方法

單例模式適用場景

一、系統只須要一個實例對象,好比系統要求提供一個惟一的序列號生成器或資源管理器,或者考慮資源消耗太大隻容許建立一個對象。

二、用戶調用單例類的的實例只容許使用一個公共訪問點,除了該公共訪問點,不能經過其它路徑訪問該實例。

更多問題

如何對單例模式進行改造,使得系統中某個類的對象能夠有有限多個?例如二例、三例等。(改造後的類能夠稱爲多例類)

相關文章
相關標籤/搜索