【設計模式】單例設計模式實現總結

單例模式的總體概述

單例模式,屬於創建型模式,《設計模式》一書對它做了定義:保證一個類僅有一個實例,並提供一個全局訪問點。

單例模式適用於無狀態的工具類、全局信息類等場景。例如日誌工具類,在系統中記錄日誌;假設我們需要統計網站的訪問次數,可以設置一個全局計數器。

單例模式的優勢有

  • 在內存裏只有一個實例,減少了內存開銷;
  • 可以避免對資源的多重佔用;
  • 設置全局訪問點,嚴格控制訪問。

單例模式的研究重點大概有以下幾個:

  1. 構造私有,提供靜態輸出接口
  2. 線程安全,確保全局唯一
  3. 延遲初始化
  4. 防止反射攻擊
  5. 防止序列化破壞單例模式

多種實現方式與比較

線程安全的餓漢模式

public class HungrySingleton {
    private final static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return instance;
    }
}

也可以通過靜態代碼塊的形式實現。實現與靜態常量基本相同,只是把實例化過程放到了靜態代碼塊中。

private final static HungrySingleton2 instance;
static {
    instance = new HungrySingleton2();
}

餓漢單例模式的特點有

  • 實現簡單
  • 線程安全
  • 類加載時初始化實例

線程安全的懶漢單例模式

懶漢式用於解決延遲初始化問題,用到了才實例化。

public class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {

    }

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

對於多線程來說,上面的實現存在競態條件:先檢查後執行,無法保證全局唯一。

通過給getInstance方法添加synchronized修飾,或者同步代碼塊形式很容易實現線程安全,保證全局唯一。

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

然而代碼會對性能造成影響,在第一個實例創建成功後,我們便不再需要鎖。因此外層再對instance做一次空判斷,即雙重檢查鎖定。

雙重檢查鎖定和volatile優化

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton instance;

    private LazyDoubleCheckSingleton() {
    }

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

但是JVM即時編譯器中存在指令重排序優化。instance賦值語句包含了下面3個操作:

  1. 分配內存給對象
  2. 初始化對象
  3. 設置instance引用,指向剛分配的內存地址

程序執行時,步驟2和3可能出現重排序,導致instance先指向了內存地址,再初始化對象。其他線程外層校驗是instance不爲空,調用未完成初始化對象的方法會報空指針異常。
clipboard

禁止指令重排序是volatile的兩大特性之一。使用volatile修飾instance,在賦值操作後加入內存柵欄,賦值之前的所有操作均可見。

靜態內部類實現延遲加載

public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton() {
    }

    private static class InnerClass {
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.INSTANCE;
    }
}

靜態內部類的優點是:外部類加載時並不需要立即加載內部類,內部類不被加載則不去初始化INSTANCE,故而不佔內存。即當外部類第一次被加載時,並不需要去加載InnerClassr,只有當getInstance()方法第一次被調用時,纔會去初始化INSTANCE,第一次調用getInstance()方法會導致虛擬機加載InnerClass類,這種方法不僅能確保線程安全,也能保證單例的唯一性,同時也延遲了單例的實例化。

在靜態內部類的初始化階段(class文件被加載後,被線程使用之前),執行類的初始化,JVM會獲取一個類Class對象的初始化鎖,鎖可以同步多個線程對一個類的初始化。因此,類初始化允許重排序,非構造線程是無法看到重排序的。
重排序1

單元素枚舉實現單例模式

《Effective Java》推薦使用單元素枚舉類型實現Singleton,書中這樣描述“功能上與公有域方法類似,但更加簡潔無償地提供了序列化機制,絕對防止多次實例化”。

public enum EnumSingleton {
    INSTANCE {
        @Override
        protected void print() {
            System.out.println("使用枚舉構建單例模式");
        }
    };

    protected abstract void print();

    public static EnumSingleton getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        EnumSingleton instance = EnumSingleton.getInstance();
        instance.print();
    }
}

反編譯代碼

public abstract class EnumSingleton extends Enum
{

    public static EnumSingleton[] values()
    {
        return (EnumSingleton[])$VALUES.clone();
    }

    public static EnumSingleton valueOf(String name)
    {
        return (EnumSingleton)Enum.valueOf(singleton/EnumSingleton, name);
    }

    private EnumSingleton(String s, int i)
    {
        super(s, i);
    }

    protected abstract void print();

    public static EnumSingleton getInstance()
    {
        return INSTANCE;
    }

    public static void main(String args[])
    {
        EnumSingleton instance = getInstance();
        instance.print();
    }

    public static final EnumSingleton INSTANCE;
    private static final EnumSingleton $VALUES[];

    static 
    {
        INSTANCE = new EnumSingleton("INSTANCE", 0) {
            protected void print()
            {
                System.out.println("enum singleton");
            }
        };
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}

從反編譯出的代碼中可以看出,EnumInstance類在加載時,就把INSTANCE屬性初始化好了,和餓漢模式類似。

  • 類final–不能被繼承
  • 構造器私有–不能外部實例化
  • 類變量是靜態的–類加載初始化

【注】在EnumInstance類中,不定義print()方法的話,class反編譯後是final類型的。

各種實現方式的選取

最好的實現方式是枚舉,可以避免反射和序列化對單例模式的破壞;不能使用線程不安全的實現方式;如果程序一開始要加載的資源太多,就應該選取懶加載;餓漢單例模式在對象創建需要配置文件時不適用。

下一小節:《如何避免反射和序列化破壞單例模式》

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