1. 程式人生 > >單例模式實現總結

單例模式實現總結

前言

設計模式是大牛們總結的一套解決特定問題程式設計模式,它主要分為建立型、結構型和行為型三大類,建立型中的單例模式是開發中最常見的。它主要用在有些物件資源的建立和銷燬非常消耗資源,最好整個系統只有一個物件。現在來總結一下程式碼中常用的幾種實現方式。

餓漢式

在Java當中通過設定建構函式為private訪問許可權確保使用者無法建立單例物件。為了讓使用者能夠訪問到單例物件還需要提供一個靜態函式介面用來返回單例物件。由於JVM虛擬機器在初始化類物件的時候會確保在多執行緒情況下的同步,因而即使在多執行緒情況下依然可以保證單例物件的唯一性。

public class EagerSingleton {
    // 餓漢式
private static final EagerSingleton INSTANCE = new EagerSingleton(); public static EagerSingleton getInstance() { return INSTANCE; } private EagerSingleton() { } }

不過這種實現上有一個問題,假如有使用者將單例物件序列化到磁碟上,然後再通過讀取序列化的物件重新生成單例物件,這是無法保證整個JVM中只有這一個單例物件。

public class EagerSingleton
implements Serializable {
// 餓漢式 private static final EagerSingleton INSTANCE = new EagerSingleton(); public static EagerSingleton getInstance() { return INSTANCE; } private EagerSingleton() { } public static void main(String[] args) throws Exception { ObjectOutputStream oos = new
ObjectOutputStream(new FileOutputStream("test.txt")); oos.writeObject(EagerSingleton.getInstance()); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.txt")); EagerSingleton singleton = (EagerSingleton) ois.readObject(); // 列印false System.out.println(singleton == EagerSingleton.getInstance()); } }

在物件的序列化可以使用readResolve來替換讀取到的實際值,所以為這個單例新增上readResolve方法就可以解決多個單例的問題。

public class EagerSingleton implements Serializable {
    // 餓漢式
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }

    private EagerSingleton() {

    }

    private Object readResolve() {
        return getInstance();
    }
}

懶漢式

餓漢式對於簡單的單例物件能夠有較好的效果,單例的物件初始化特別耗時,比如有些不常用的Android模組可能需要載入so檔案,如果還是一開始就初始化必定導致程式啟動慢,對這類物件可以採用懶漢式載入,也就是需要使用到單例物件的時候在初始化。

public class LazySingleton {
    // 懶漢式
    private static LazySingleton INSTANCE;

    // 這個synchronized很重要
    public static synchronized LazySingleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new LazySingleton();
        }
        return INSTANCE;
    }

    private LazySingleton() {

    }
}

懶漢式之所以要加上synchronized關鍵字是確保在多執行緒情況下只有加鎖成功的執行緒才執行初始化操作,這樣就確保了單例物件的唯一性。可以看出這個鎖物件其實只在初始化的時候才有必要,以後讀取都不需要鎖操作。如果訪問單例物件非常頻繁那就需要不停的加解鎖,影響程式碼效能,為此又有高手開發除了雙重檢查單例實現。

DCL(Double Check Lock)

所謂的雙重檢查是指在讀取單例物件為空時在做鎖定操作,鎖定之後再檢查單例物件是否為空,如果為空再進行初始化操作。但是最終這種實現被證明是有問題的,因為JVM內部的多執行緒可見性和重排序會導致使用者引用未初始化完全的單例物件。Java5.0之後JVM增強了volatile關鍵字的實現,確保volatile型別的欄位不會在未完全初始化之前對其他執行緒課件。

public class DCLSingleton {
    // 注意這個volatitle關鍵字
    private static volatile DCLSingleton INSTANCE;

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

    private DCLSingleton() {

    }
}

之所以出現上面提到的其他執行緒可能讀到初始化執行緒還未初始化完的單例物件,主要因為Java中的物件賦值操作其實並不是原子操作,而是分成了三個步驟,而JVM中又支援指令重排序以提高程式執行的效率。

指令序號 正常指令 重排序指令
1 分配物件記憶體 分配物件記憶體
2 初始化物件 將記憶體賦值給引用
3 將記憶體賦值給引用 初始化物件

假如JVM重排序後的指令執行到第二條完成,這個時候發生了執行緒切換,另外一個執行緒去讀取單例物件引用會發現非空,但是裡面引用的值都是未初始化的,這個時候假如單例物件有一個屬性正好是物件型別訪問這個屬性就會丟擲空指標異常。而5.0增強的volatile關鍵字禁止了初始化指令重排序到賦值給引用之前。

Holder內部類實現

前面的餓漢式實現中提到JVM確保載入的類物件內部初始化是同步的,也就是說如果多個執行緒同時要初始化一個類那個只有先獲得JVM初始化鎖的執行緒才會真正執行初始化類程式碼,從而保證JVM中的類物件只有一個。可以在單例類內部定義一個Holder類,這個Holder內部正好只有一個單例物件的引用,當用戶獲取單例物件時JVM載入內部類同時初始化它的靜態成員保證單例物件的唯一性。

public class HolderSingleton {

    public static HolderSingleton getInstance() {
        return HolderSingletonHolder.INSTANCE;
    }

    private static final class HolderSingletonHolder {
        private static final HolderSingleton INSTANCE = new HolderSingleton();
    }

    private HolderSingleton() {

    }
}

課件這個實現非常的簡潔,而且它實現了懶載入效果,能夠優化大物件的初始化。

enum單例

前面的幾種實現方式都有序列化和反序列化的問題,也就是說如果單例物件從別的地方反序列化回來一個物件可能導致多個初始化的單例物件,需要覆蓋readResolve方法,而enmu本身就支援序列化和反序列化,而且它的初始化全部由JVM控制不會出現多執行緒問題。

public enum EnumSingleton {
    INSTANCE
}