單例模式
單例模式應該是作為開發最早接觸的設計模式了。確定某一個類隻有一個執行個體,而且自行執行個體化并向整個系統提供這個執行個體就是單例模式。
什麼時候需要使用單例呢?
確定某個類有且隻有一個對象的場景,避免産生多個對象消耗過多的資源,或當建立一個對象需要消耗的資源過多時,如通路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件事情:
- 給DCLSingleton執行個體配置設定記憶體;
- 調用DCLSingleton()的構造函數,初始化成員字段;
- 将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選擇提示的模闆快速生成單例的代碼。