1. 程式人生 > >單例(Singleton)

單例(Singleton)

1. 單例(Singleton)

Intent

確保一個類只有一個例項,並提供該例項的全域性訪問點。

Class Diagram

使用一個私有建構函式、一個私有靜態變數以及一個公有靜態函式來實現。

私有建構函式保證了不能通過建構函式來建立物件例項,只能通過公有靜態函式返回唯一的私有靜態變數。

 

Implementation

Ⅰ 懶漢式-執行緒不安全

以下實現中,私有靜態變數 uniqueInstance 被延遲例項化,這樣做的好處是,如果沒有用到該類,那麼就不會例項化 uniqueInstance,從而節約資源。

這個實現在多執行緒環境下是不安全的,如果多個執行緒能夠同時進入 if (uniqueInstance == null)

 ,並且此時 uniqueInstance 為 null,那麼會有多個執行緒執行 uniqueInstance = new Singleton(); 語句,這將導致例項化多次 uniqueInstance。

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

Ⅱ 餓漢式-執行緒安全

執行緒不安全問題主要是由於 uniqueInstance 被例項化多次,採取直接例項化 uniqueInstance 的方式就不會產生執行緒不安全問題。

但是直接例項化的方式也丟失了延遲例項化帶來的節約資源的好處。

private static Singleton uniqueInstance = new Singleton();

Ⅲ 懶漢式-執行緒安全

只需要對 getUniqueInstance() 方法加鎖,那麼在一個時間點只能有一個執行緒能夠進入該方法,從而避免了例項化多次 uniqueInstance。

但是當一個執行緒進入該方法之後,其它試圖進入該方法的執行緒都必須等待,即使 uniqueInstance 已經被例項化了。這會讓執行緒阻塞時間過長,因此該方法有效能問題,不推薦使用。

public static synchronized Singleton getUniqueInstance() {
    if (uniqueInstance == null) {
        uniqueInstance = new Singleton();
    }
    return uniqueInstance;
}

Ⅳ 雙重校驗鎖-執行緒安全

uniqueInstance 只需要被例項化一次,之後就可以直接使用了。加鎖操作只需要對例項化那部分的程式碼進行,只有當 uniqueInstance 沒有被例項化時,才需要進行加鎖。

雙重校驗鎖先判斷 uniqueInstance 是否已經被例項化,如果沒有被例項化,那麼才對例項化語句進行加鎖。

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

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

考慮下面的實現,也就是隻使用了一個 if 語句。在 uniqueInstance == null 的情況下,如果兩個執行緒都執行了 if 語句,那麼兩個執行緒都會進入 if 語句塊內。雖然在 if 語句塊內有加鎖操作,但是兩個執行緒都會執行 uniqueInstance = new Singleton(); 這條語句,只是先後的問題,那麼就會進行兩次例項化。因此必須使用雙重校驗鎖,也就是需要使用兩個 if 語句。

if (uniqueInstance == null) {
    synchronized (Singleton.class) {
        uniqueInstance = new Singleton();
    }
}

uniqueInstance 採用 volatile 關鍵字修飾也是很有必要的, uniqueInstance = new Singleton(); 這段程式碼其實是分為三步執行:

  1. 為 uniqueInstance 分配記憶體空間
  2. 初始化 uniqueInstance
  3. 將 uniqueInstance 指向分配的記憶體地址

但是由於 JVM 具有指令重排的特性,執行順序有可能變成 1>3>2。指令重排在單執行緒環境下不會出現問題,但是在多執行緒環境下會導致一個執行緒獲得還沒有初始化的例項。例如,執行緒 T1 執行了 1 和 3,此時 T2 呼叫 getUniqueInstance() 後發現 uniqueInstance 不為空,因此返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保證在多執行緒環境下也能正常執行。

Ⅴ 靜態內部類實現

當 Singleton 類載入時,靜態內部類 SingletonHolder 沒有被載入進記憶體。只有當呼叫 getUniqueInstance() 方法從而觸發 SingletonHolder.INSTANCE 時 SingletonHolder 才會被載入,此時初始化 INSTANCE 例項,並且 JVM 能確保 INSTANCE 只被例項化一次。

這種方式不僅具有延遲初始化的好處,而且由 JVM 提供了對執行緒安全的支援。

public class Singleton {

    private Singleton() {
    }

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getUniqueInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Ⅵ 列舉實現

public enum Singleton {

    INSTANCE;

    private String objName;


    public String getObjName() {
        return objName;
    }


    public void setObjName(String objName) {
        this.objName = objName;
    }


    public static void main(String[] args) {

        // 單例測試
        Singleton firstSingleton = Singleton.INSTANCE;
        firstSingleton.setObjName("firstName");
        System.out.println(firstSingleton.getObjName());
        Singleton secondSingleton = Singleton.INSTANCE;
        secondSingleton.setObjName("secondName");
        System.out.println(firstSingleton.getObjName());
        System.out.println(secondSingleton.getObjName());

        // 反射獲取例項測試
        try {
            Singleton[] enumConstants = Singleton.class.getEnumConstants();
            for (Singleton enumConstant : enumConstants) {
                System.out.println(enumConstant.getObjName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
firstName
secondName
secondName
secondName

該實現在多次序列化再進行反序列化之後,不會得到多個例項。而其它實現需要使用 transient 修飾所有欄位,並且實現序列化和反序列化的方法。

該實現可以防止反射攻擊。在其它實現中,通過 setAccessible() 方法可以將私有建構函式的訪問級別設定為 public,然後呼叫建構函式從而例項化物件,如果要防止這種攻擊,需要在建構函式中新增防止多次例項化的程式碼。該實現是由 JVM 保證只會例項化一次,因此不會出現上述的反射攻擊。

Examples

  • Logger Classes
  • Configuration Classes
  • Accesing resources in shared mode
  • Factories implemented as Singletons

JDK