簡單易懂的Java設計模式之單例模式

定義

Java中單例模式定義:“一個類有且僅有一個實例,並且自行實例化向整個系統提供。”

寫法

1.餓漢式

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }

}

這種方式在類加載時就完成了初始化,所以類加載較慢,但獲取對象的速度快。這種方式避免了多線程的同步問題。在類加載的時候就完成實例化,沒有達到懶加載的效果。如果從始至終未使用過這個實例,則會造成內存的浪費。

2.懶漢式(線程不安全)

public class Singleton {

    private static Singleton instance;

    private Singleton() {
        
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

}

懶漢模式聲明瞭一個靜態對象,在第一次調用時初始化。這雖然節約了資源,但第一次加載時需要實例化,反應稍慢一些,而且在多線程時不能正常工作。

3.懶漢式(線程安全)

public class Singleton {

    private static Singleton instance;

    private Singleton() {

    }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

}

種寫法能夠在多線程中很好地工作,但是每次調用getInstance方法時都需要進行同步。這會造成不必要的開銷,而且大部分時候我們是用不到同步的。所以,不太建議用這種模式。

4.雙重檢查模式(DCL)

public class Singleton {

    private volatile static Singleton instance;

    private Singleton() {

    }

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

}

這種寫法在getInstance方法中對Singleton進行了兩次判空:第一次是爲了不必要的同步,第二次是在instance等於null的情況下才創建實例。

DCL的優點是資源利用率高。第一次執行getInstance時單例對象才被實例化,效率高。其缺點是第一次加載時反應稍慢一些,在高併發環境下也有一定的缺陷。DCL雖然在一定程度上解決了資源的消耗和多餘的同步、線程安全等問題,但其還是在某些情況會出現失效的問題,也就是DCL失效。這裏建議用靜態內部類單例模式來替代DCL。

這裏爲什麼要使用volatile呢?
上面這種寫法看似很完美,但實際上仍然存在一個問題——當instance不爲null時,可能指向一個"被部分初始化的對象"。問題出在這行簡單的賦值語句:

instance = new Singleton();

它並不是一個原子操作。可以”抽象“爲下面幾條JVM指令:

memory = allocate();    //1:分配對象的內存空間
ctorInstance(memory);   //2:初始化對象
instance = memory;      //3:設置instance指向剛分配的內存地址

上面2依賴於1,但是3並不依賴於2,所以JVM可以以“優化”爲目的對它們進行重排序,經過重排序後如下:

memory = allocate();    //1:分配對象的內存空間
instance = memory;      //3:設置instance指向剛分配的內存地址(此時對象還未初始化)
ctorInstance(memory);   //2:初始化對象

可以看到指令重排序之後, 3 排在了 2 之前,即引用instance指向內存memory時,這段嶄新的內存還沒有初始化——即,引用instance指向了一個"被部分初始化的對象"。此時,如果另一個線程調用getInstance方法,由於instance已經指向了一塊內存空間,從而if條件判爲false,方法返回instance引用,用戶得到了沒有完成初始化的“半個”單例。

解決該問題,只需要將instance聲明爲volatile變量。在這裏使用volatile會或多或少地影響性能,但考慮到程序的正確性,犧牲這點性能還是值得的。

5.靜態內部類單例

public class Singleton {

    private Singleton() {

    }

    public static Singleton getInstance() {
        return SingletonLazyHolder.sInstance;
    }

    private static class SingletonLazyHolder {
        private static final Singleton sInstance = new Singleton();
    }

}

第一次加載Singleton類時並不會初始化sInstance,只有第一次調用getInstance方法時虛擬機加載SingletonLazyHolder 並初始化 sInstance。這樣不僅能確保線程安全,也能保證Singleton 類的唯一性。所以,推薦使用靜態內部類單例模式。

6.枚舉單例

public enum Singleton {

    INSTANCE
    /*其他方法*/
}

上面就是枚舉單例模式,需要小心的是如果你在使用實例方法,那麼你需要確保線程安全。默認枚舉實例的創建是線程安全的,但是在枚舉中的其他任何方法由開發人員自己處理。枚舉單例的優點就是簡單,但是大部分應用開發很少用枚舉,其可讀性並不是很高。

使用場景

單例模式可能的使用場景如下:
1.整個項目需要一個共享訪問點或共享數據。
2.創建一個對象需要耗費的資源過多,比如訪問I/O或者數據庫等資源。
3.工具類對象等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章