天天看點

單例模式

單例及單例模式概念

一個類隻允許建立一個執行個體。這種設計模式就是單例模式

單例模式

實作單例模式需要考慮什麼?

  • 構造函數私有,避免外部通過new來建立執行個體
  • 考慮是否需要延時加載
  • 考慮線程安全

單例模式的幾種實作方式

1. 餓漢式:在類加載的時候,就已經初始化執行個體了。

public class Hungry {
  // 一啟動就被加載,還未使用就占用記憶體,造成資源浪費
  private static final Hungry INSTANCE = new Hungry();
  
  // 構造方法私有,外部無法進行執行個體化
  private Hungry() {}
  
  // 擷取執行個體對象
  public static Hungry getInstance() {
    return INSTANCE;
  }
}      

2. 懶漢式:第一次使用時才進行執行個體化

public class Lasy {
    // 啟動時不加載,需要時再進行執行個體化
    private static Lasy INSTANCE;
    // 構造方法私有,外部無法進行執行個體化
    private Lasy() {}
    public static Lasy getInstance() {
        if (null == INSTANCE) {
            INSTANCE = new Lasy();
        }
        return INSTANCE;
    }
}      

但是懶漢式并不能保證單例,在多線程并發情況下,會多次調用構造方法進行執行個體化,示例如下:

public class Lasy {
    // 啟動時不加載,需要時再進行執行個體化
    private static Lasy INSTANCE;
    // 構造方法私有,外部無法進行執行個體化
    private Lasy() {
        System.out.println(Thread.currentThread().getName());
    }
    public static Lasy getInstance() {
        if (null == INSTANCE) {
            INSTANCE = new Lasy();
        }
        return INSTANCE;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(Lasy::getInstance).start();
        }
    }
}      

輸出結果:

單例模式

2.1 使用同步鎖synchronized

//如果使用synchronized關鍵字,這種方式在并發下并發量等同于1,如果頻繁使用該對象,會大大降低性能
public static synchronized Lasy getInstance() {
    if (null == INSTANCE) {
        INSTANCE = new Lasy();
    }
    return INSTANCE;
}      

2.2 雙重檢測(DCL懶漢式單例)

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

// INSTANCE = new Lasy();
/** 以上操作并不是原子性操作,包括以下三個步驟:
  * 1. 配置設定記憶體空間
  * 2. 執行構造方法進行初始化
  * 3. 将對象指向這個空間
  */
/** 在多線程情況下可能出現指令重排,每個線程執行INSTANCE = new Lasy()這個操作的順序可能不一緻,那麼就有可能出現線程A先執行了第一步和第三步,此時還未進行初始化,線程B執行判斷語句null == INSTANCE時為false,将直接傳回線程A未進行初始化的INSTANCE對象,是以要避免指令重排,使用volatile關鍵字。*/

private static volatile Lasy INSTANCE;

public static Lasy getInstance() {
    if (null == INSTANCE) {
        // 對象未被執行個體化,等待類對象鎖
        synchronized (Lasy.class) {
            // 進入對象鎖,其他線程等待,再次判斷是否被執行個體化
            if (null == INSTANCE) {
                INSTANCE = new Lasy();
            }
        }
    }
    return INSTANCE;
}      

餓漢式和懶漢式比較:

①由于餓漢式是在類加載的時候進行執行個體化,線程安全;懶漢式是在第一次使用的時候進行執行個體化,此時可能多線程會同時進行執行個體化,是以線程不安全;

②餓漢式不支援延時加載,如果占用資源較多或者耗時較長,可能會影響啟動時間;懶漢式支援延時加載,但是需要使用同步鎖,如果這個類被頻繁使用,可能導緻性能問題。(相比較更加傾向于餓漢式單例,在啟動時進行加載,如果遇到性能或者資源問題會提前暴露,相較于正在運作中出現問題更加可控)

3. 靜态内部類

public class Test {
    private Test() {}

    private static class InnerTest{
        // final修飾變量是不可變的,初始化對象之後便不會再指向另一個對象
        private static final Test INSTANCE = new Test();
    }

    public static Test getInstance() {
        return InnerTest.INSTANCE;
    }
}      

但是在反射機制下,所有的私有方法都能被破解,如下執行個體表示通過靜态内部類獲得的執行個體對象和通過反射獲得的holder執行個體對象并不是同一個,仍然破壞了單例

public static void main(String[] args) throws Exception {
    // 擷取private的構造方法
    Constructor<Test> constructor = Test.class.getDeclaredConstructor(null);
    // 無視私有構造器
    constructor.setAccessible(true);
    Test test = constructor.newInstance();

    System.out.println(Test.getInstance().equals(test));
}

輸出結果:false      

通過反射的源碼可以看到,無法反射式建立枚舉對象,是以我們可以考慮使用枚舉來實作單例

單例模式

4. 枚舉

public enum SingleEnum {
  INSTANCE;
}      

以下示例可見确實無法反射式建立枚舉對象,當使用反射建立枚舉對象時,會抛出異常資訊Cannot reflectively create enum objects

public static void main(String[] args) throws Exception {
    // 擷取private的構造方法(實際上枚舉類的構造方法帶有兩個參數,并不是我們從源碼看到的無參構造,需要使用源碼工具來檢視)
    Constructor<SingleEnum> constructor = SingleEnum.class.getDeclaredConstructor(String.class, int.class);
    // 無視私有構造器
    constructor.setAccessible(true);
    SingleEnum single = constructor.newInstance();

    System.out.println(SingleEnum.INSTANCE.equals(single));
}      

輸出:

單例模式

繼續閱讀