天天看點

單例設計模式總結單例模式代碼模闆

單例模式

單例模式應該是作為開發最早接觸的設計模式了。確定某一個類隻有一個執行個體,而且自行執行個體化并向整個系統提供這個執行個體就是單例模式。

什麼時候需要使用單例呢?

確定某個類有且隻有一個對象的場景,避免産生多個對象消耗過多的資源,或當建立一個對象需要消耗的資源過多時,如通路IO和資料庫等資源。

實作單例的關鍵點

  • 構造函數不對外開放,一般為private;
  • 通過一個靜态方法或者枚舉傳回單例類對象;
  • 確定單例類的對象有且隻有一個,尤其是在多線程環境下;
  • 確定單例類對象在反序列化時不會重新建構對象;

實作方式

餓漢式單例

public class EagerSingleton {
    private static final EagerSingleton sInstance = new EagerSingleton();

    private EagerSingleton() {
    }

    public static EagerSingleton getInstance() {
        return sInstance;
    }
}
複制代碼
           

餓漢式是最簡單的單例,它的執行個體在系統啟動的時候就會初始化,并且是線程安全,因為在通路之前就初始化好了,不存在同步的問題。但是這樣也代表着,如果這個單例并不一定會用到,或者隻有特定的地方才會使用,并且消耗的資源很多,那麼一開始就初始化執行個體并不是一個聰明的舉動,代表着資源的浪費。

懶漢式單例

public class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {
    }

    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
複制代碼
           

懶漢式會在首次調用getInstance()方法的時候初始化執行個體,并且synchronized修飾方法,確定了多線程情況下的單例對象唯一性。但這也帶來了問題,即instance在初始化之後的每次調用getInstance()都會進行同步,這樣會消耗不必要的資源。

Double Check Lock (DCL)單例

public class DCLSingleton {
    private static DCLSingleton instance;
    
    private DCLSingleton() {
    }
    
    public static DCLSingleton getInstance() {
        if (instance == null) {
            synchronized (DCLSingleton.class) {
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}
複制代碼
           

可以看到DCL其實與懶漢式類似,隻是在getInstance方法上做了優化,可以看到getInstance方法中對instance進行了兩次判空:第一層判斷主要是為了避免不必要的同步,第二層的判斷則是為了在null的情況下建立執行個體。

但是DCL單例也有隐藏的隐患。

假設線程A執行到

instance = new DCLSingleton();

語句,這裡看起來是一句代碼,但實際上它并不是一個原子操作,這句代碼最終會被編譯成多條彙編指令,它大緻做了3件事情:

  1. 給DCLSingleton執行個體配置設定記憶體;
  2. 調用DCLSingleton()的構造函數,初始化成員字段;
  3. 将instance對象指向配置設定的記憶體空間(此時instance就不是null了)。

但是,由于Java編譯器允許處理器亂序執行,以及JDK1.5之前JVM(Java記憶體模型)中Cache、寄存器到主記憶體回寫順序的規定,上面的2和3的順序是無法保證的。也就是說,執行順序可能是1-2-3也可能是1-3-2.如果是後者,并且在3執行完畢,2未執行之前,被切換到線程B上,這時候instance因為已經線上程A内執行過了3,instance已經是非空了,是以,線程B直接取走instance,在使用時就會出錯,這就是DCL失效問題,而且這種難以追蹤難以重制的錯誤很可能會隐藏很久。

在JDK1.5之後,SUN官方已經注意到這種問題,調整了JVM ,具體化了volatile關鍵字,是以,如果是1.5之後的版本,隻需要将instance定義改成

private volatile static DCLSingleton instance;

就可以保證instance對象每次都是從主記憶體中讀取,就可以使用DCL的寫法來完成單例模式。當然,volatile或多或少也會影響性能。

靜态内部類單例

public class StaticSingleton {
    private StaticSingleton() {
    }

    public static StaticSingleton getInstance() {
        return SingletonHolder.sInstance;
    }

    private static class SingletonHolder {
        private static final StaticSingleton sInstance = new StaticSingleton();
    }
}
複制代碼
           

當第一次加載StaticSingleton類時并不會初始化sInstance,隻有在第一次調用getInstance方法時才會導緻sInstance被初始化。是以,第一次調用getInstance方法或導緻虛拟機加載SingletonHolder類,這種方式不僅能夠確定線程安全,也能夠保證單例對象的唯一性,同時也延遲了單例的執行個體化。這也是本人最喜歡的單例方式。

使用容器實作單例

public class SingletonManager {
    private static Map<String, Object> objectMap = new HashMap<>();
    
    private SingletonManager() {
    }

    public static void registerService(String key, Object instance) {
        if (!objectMap.containsKey(key)) {
            objectMap.put(key, instance);
        }
    }

    public static Object getService(String key) {
        return objectMap.get(key);
    }
}
複制代碼
           

這是一種另類的實作,在程式的初始,将多種單例類型注入到一個統一的管理類中,在使用時根據key擷取對象對應類型的對象。這種方式使得我們可以管理多種類型的單例,并且在使用時可以通過統一的接口進行擷取操作,降低了使用者的使用成本,也對使用者隐藏了具體實作,降低了耦合度。

在Android系統中的各種Service就是通過這種方式管理的單例。

ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
複制代碼
           

枚舉單例

public enum EnumSingleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("do sth.");
    }
}
複制代碼
           

沒錯,就是枚舉!

寫法簡單是枚舉單例最大的優點,枚舉在Java中與普通的類是一樣的,不僅能夠有字段,還能夠有自己的方法。最重要的時預設枚舉執行個體的建立是線程安全的,并且在任何情況下它都是一個單例。

為什麼這麼說呢?上面幾種單例的實作中,在一個情況下他們會出現重新建立對象的情況,那就是反序列化。

實作序列化的單例

通過序列化可以将一個單例的執行個體對象寫到磁盤,然後在讀回來,進而有效地獲得一個執行個體。即使構造函數是私有的,反序列化時依然可以通過特殊的途徑去建立類的一個新執行個體,相當于調用該類的構造函數。反序列化操作提供了一個很特别的鈎子函數,類中具有一個私有的、被執行個體化的方法readResolve(),這個方法可以讓開發人員控制對象的反序列化。

也就是說如果你的單例實作了Serializable接口,那麼為了保證單例也必須添加readResolve()方法控制反序列化傳回的對象。例如下面這個例子:

public class EagerSingleton implements Serializable {
    private static final EagerSingleton sInstance = new EagerSingleton();

    private EagerSingleton() {
    }

    public static EagerSingleton getInstance() {
        return sInstance;
    }

    //支援序列化的單例
    private Object readResolve() throws ObjectStreamException {
        return sInstance;
    }
}
複制代碼
           

代碼模闆

在as中,我們可以為單例設定代碼模闆,加快我們單例類的編寫。

在設定中找到Live Templates

點選标記1的加号一次添加分類以及模闆,例如我建立的分類myTemplate以及模闆singleton(标記2)。

标記3的位置是提示的前提,也就是說這裡設定了什麼内容,你在代碼中敲出同樣内容後,就會提示代碼模闆:

标記4的區域是模闆的内容,這裡我選用的靜态内部類的單例模式:

private $CLASS$(){
}

public static $CLASS$ getInstance(){
    return SingletonHolder.sInstance;
}

private static class SingletonHolder{
    private static final $CLASS$ sInstance = new $CLASS$();
}
複制代碼
           

$CLASS$

可以被動态替換為所在的類名,當我在一個新的類中輸入singleton,并選擇了模闆,就會生成這些代碼。

标記5 可以選擇模闆提示的範圍,這裡我們選擇在Java的代碼中生效:

最後點選确定儲存模闆。接下來就可以在代碼中敲出singleton選擇提示的模闆快速生成單例的代碼。