Java設計模式優化-單例模式

單例模式概述

單例模式是一種對象建立模式,用於產生一個類的具體事例。使用單例模式能夠確保整個系統中單例類只產生一個實例。有下面兩大好處:java

  1. 對於頻繁建立的對象,節省初第一次實例化以後的建立時間。
  2. 因爲new操做的減小,會下降系統內存的使用頻率。減輕GC壓力,從而縮短GC停頓時間

建立方式:安全

  1. 單例做爲類的私有private屬性
  2. 單例類擁有私有private構造函數
  3. 提供獲取實例的public方法

單例模式的角色:性能優化

角色 做用
單例類 提供單例的工廠,返回類的單例實例
使用者 獲取並使用單例類

類基本結構:
單例模式類圖多線程

單例模式的實現

1.餓漢式

public class HungerSingleton {
    //1.餓漢式
    //私有構造器
    private HungerSingleton() {
        System.out.println("create HungerSingleton");
    }
    //私有單例屬性
    private static HungerSingleton instance = new HungerSingleton();
    //獲取單例的方法
    public static HungerSingleton getInstance() {
        return instance;
    }
}

注意:jvm

  1. 單例修飾符爲static JVM加載單例類加載時,直接初始化單例。沒法延時加載。若是此單例一直未被使用,單Singleton 由於調用靜態方法被初始化則會形成內存的浪費。
  2. getInstance()使用static修飾,不用實例化能夠直接使用Singleton.getInstance()獲取單例。
  3. 因爲單例由JVM加載類的時候建立,因此不存在線程安全問題。

2.簡單懶漢式

public class Singleton {
    //2.1簡單懶漢式(線程不安全)
    //私有構造器
    private Singleton() {
        System.out.println("create Singleton");
    }
    //私有單例屬性[初始化爲null]
    private static Singleton instance = null;
    //獲取單例的方法
    public static Singleton getInstance() {
        if(instance == null) {
            //此處instance實例化
            //首次調用單例時會進入  達成延時加載
            instance = new Singleton();
        }
        return instance;
    }
}
  • 因爲未使用 synchronized 關鍵字,因此當線程1調用單例工廠方法Singleton.getInstance() 且 instance 未初始化完成時,線程2調用此方法會將instance判斷爲null,也會將instance從新實例化賦值,此時則產生了多個實例!
  • 如需線程安全能夠直接給getInstance方法上加synchronized關鍵字,以下:
public class Singleton {
    //2.2簡單懶漢式(線程安全)
    //私有構造器
    private Singleton() {
        System.out.println("create Singleton");
    }
    //私有單例屬性[初始化爲null]
    private static Singleton instance = null;
    //獲取單例的方法 將此方法使用synchronized關鍵字同步
    public static synchronized Singleton getInstance() {
        if(instance == null) {
            //此處instance實例化
            //首次調用單例時會進入  達成延時加載
            instance = new Singleton();
        }
        return instance;
    }
}

面臨的問題:函數

  • 因爲對getInstance()整個方法加鎖,在多線程的環境中性能比較差。

3.DCL 懶漢式(雙重檢測)

簡單懶漢式(線程安全)中,對getInstance()方法加鎖,致使多線程中性能較差,那麼是否能夠 減少鎖的範圍,使不用每次調用geInstance()方法時候都會去競爭鎖?

DCL(Double Check Locking)雙重檢測 就是這樣一種實現方式。性能

public class DCLLazySingleton {
    //3.DCL
    //私有構造器
    private DCLLazySingleton() {
        System.out.println("create DCLLazySingleton");
    }
    //私有單例屬性[初始化爲null] volatile 保證內存可見性 防止指令重排
    private static volatile DCLLazySingleton instance = null;//step1
    //獲取單例的方法
    public static DCLLazySingleton getInstance() {
        //這裏判null 是爲了在instance有值時,不進入加鎖的代碼塊,提升代碼性能。
        if(instance == null) {
            //縮小鎖範圍 因爲是靜態方法方法調用的時候不依賴於實例化的對象 加鎖只能使用類
            synchronized (DCLLazySingleton.class) {
                //這裏判null 是爲了配合volatile解決多線程安全問題
                if(instance == null) {
                    instance = new DCLLazySingleton();
                }
            }
        }
        return instance;
    }
}

注意:優化

  1. 傳統DCL(step1處並未使用 volatile 或使用了但在JDK1.8以前)面臨的問題:spa

    • 因爲初始化單例對象new DCLLazySingleton() 操做並非原子操做,因爲這是不少條指令,jvm可能會亂序執行。
    • 在線程1初始化對象可能並未完成,可是此時已經instance對象已經不爲null。(已經分配了內存,可是構造方法還未執行完【可能有一些屬性的賦值未執行】)
    • 此時線程2再獲取instance 則不爲null 直接返回。那麼此時線程2獲取的則爲‘構造方法未執行完的instance對象’。則不能保證線程安全。
  2. 解決方式:.net

    • 加上volatile關鍵字,volatile保證內存可見性,內存屏障,防止指令排!
    • 加上volatile關鍵字後,線程2獲取的構造方法未執行完的instance對象,會在線程1修改以後同步到線程2(volatile 內存空間)。因此解決了線程安全問題
  3. 參考:

4.懶漢式(靜態內部類)

public class StaticSingleton {
    //私有構造器
    private StaticSingleton() {
        System.out.println("create StaticSingleton!");
    }
    //獲取單例的方法
    public static StaticSingleton getInstance() {
        return SingletonHolder.instance;
    }
    //靜態內部類 持有單例 做爲靜態屬性。
    //因爲只有在訪問屬性時纔會加載靜態類初始化instance。因此實現了懶加載。且因爲JVM保證了類的加載爲線程安全,因此爲線程安全的。
    private static class SingletonHolder {
        //私有單例屬性
        private static StaticSingleton instance = new StaticSingleton();
    }
}

注意:

  1. 因爲StaticSingleton類被加載時,內部的私有靜態類SingletonHolder並不會被加載,因此並不會初始化單例instance,當getInstance()被調用時SingletonHolder.instance 纔會加載SingletonHolder,因爲JVM保證了類的加載爲線程安全,所以線程安全。
  2. 此方式既能夠作到延時加載,也不會由於同步關鍵字影響性能。是一種比較完善的實現。推薦使用

5.枚舉單例

public enum EnumSingleton {
    INSTANCE();
    EnumSingleton() {
        System.out.println("create EnumSingleton");
    }
}
  • 線程安全,且可以抵禦反射與序列化。
  • 推薦使用

例外狀況

上述的單例實現方式仍是會面臨一些特殊狀況不能保證惟一實例:

  1. 反射調用私有構造方法。
  2. 序列化後反序列化會生成多個對象。能夠實現私有readResolve方法。readObject()如同虛設,直接使用readResolve替換本來返回值。以下:
private Object readResolve () {
    //返回當前對象
    return instance;
    }

因爲上述兩狀況比較特殊,因此沒有特別關注。

參考書籍


《Java程序性能優化》 -葛一鳴 等編著

相關文章
相關標籤/搜索