餓漢模式 懶漢模式(線程不安全) 懶漢模式(線程安全) 雙重檢查模式(DCL) 靜態內部類單例模式 枚舉類單例模式 使用容器實現單例模式 CAS實現單例模式 ...
單例模式的八種寫法
單例模式作為日常開發中最常用的設計模式之一,是最基礎的設計模式,也是最需要熟練掌握的設計模式。單例模式的定義是:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。那麼你知道單例模式有多少種實現方式嗎?以及每種實現方式的利弊呢?
- 餓漢模式
- 懶漢模式(線程不安全)
- 懶漢模式(線程安全)
- 雙重檢查模式(DCL)
- 靜態內部類單例模式
- 枚舉類單例模式
- 使用容器實現單例模式
- CAS實現單例模式
餓漢模式
代碼如下:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton () {
}
public static Singleton getInstance() {
return instance;
}
}
這種方式在類載入時就完成了實例化,會影響類的載入速度,但獲取對象的速度快。 這種方式基於類載入機制保證實例僅有一個,避免了多線程的同步問題,是線程安全的。
懶漢模式(線程不安全)
絕大多數時候,類載入的時機和對象使用的時機都是分開的,所以沒有必要在類載入的時候就去實例化單例對象。為了消除單例對象實例化對類載入的影響,引入了延遲載入,就有了懶漢模式的實現方式。代碼如下:
public class Singleton {
private static Singleton instance;
private Singleton () {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懶漢模式聲明瞭一個靜態對象,在用戶第一次調用時完成實例化,屬於延遲載入方式。而且這種方式不是線程安全。
懶漢模式(線程安全)
針對線程不安全的懶漢模式,對其中的獲取單例對象的方法增加同步關鍵字。代碼如下:
public class Singleton {
private static Singleton instance;
private Singleton () {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
這種寫法保證了線程安全,但是每次調用getInstance方法獲取單例時都需要進行同步,造成不必要的同步開銷,但實際上除了第一次實例化需要同步,其他時候都是不需要同步的。
雙重檢查模式(DCL)
既然懶漢模式中的實例化只需要在第一次的時候保證同步,那何不只在實例為空的時候加同步關鍵字呢。代碼如下:
public class Singleton {
private volatile static Singleton singleton; // 1
private Singleton () {
}
public static Singleton getInstance() {
if (instance== null) { // 2
synchronized (Singleton.class) { // 3
if (instance== null) { // 4
instance= new Singleton(); // 5
}
}
}
return singleton;
}
}
雙重檢查寫法,主要關註以上代碼中的5點:
- 聲明單例對象時加上volatile關鍵字,保證多線程的記憶體可見性,也即當在一個線程中單例對象實例化完成之後,其他線程也同時能夠看到。同時,還有更為重要的一點,下麵會說。
- 第一次檢查單例對象是否為空,判斷是否已經完成了實例化。
- 如果第一次檢查發現單例對象為空,那麼該線程就要對此單例類進行加鎖,準備進行實例化,加鎖是為了保證該線程進行實例化的時候沒有其他線程也同時進行實例化。
- 第二次檢查單例對象是否為空,則是為了避免這種情況:此時單例對象為空,兩個線程,A線程在第2步,B線程在第5步,A線程發現單例對象為空,緊接著B線程就完成了實例化,然後就會導致A線程又會走一次第5步的實例化過程,即重覆實例化。那麼加上了第二次檢查後,當A線程到第4步的時候就會發現單例對象已經實例化完成,自然不會到第5步。
- 真正的實例化操作就發生在第5步,且只發生一次。
DCL思考
在以上代碼的第一步中,我們提到volatile關鍵字,volatile關鍵字除了保證記憶體可見性,還有一點是禁止指令重排序。那麼問題出在哪裡呢?對,第5步。實際上,實例化對象的動作並不是一個原子操作,instance= new Singleton();
可以分為以下三步完成:
memory = allocate(); // 5.1:分配對象的記憶體空間
ctorInstance(memory); // 5.2:初始化對象
instance = memory; // 5.3: 設置instance指向剛分配的記憶體地址
而上面三行代碼,5.2和5.3可能發生重排序。跟著上面代碼中的第二次檢查的位置進行分析。當線程B執行到5.3之後,5.2之前時,這時候線程A首次判斷單例對象是否為空。這時候當然單例對象是不為空的,但是卻不能使用,因為單例對象還沒有被初始化呢。這既是DCL的缺陷所在,也是為什麼要對單例對象家volatile關鍵字的原因。禁止了指令重排序,自然不會出現線程A拿到一個不可用的單例對象。
靜態內部類單例模式
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
}
第一次載入Singleton類時並不會初始化sInstance,只有第一次調用getInstance方法時虛擬機載入SingletonHolder 並初始化sInstance ,這樣不僅能確保線程安全也能保證Singleton類的唯一性,所以推薦使用靜態內部類單例模式。
枚舉類單例模式
public enum Singleton {
INSTANCE;
public void doSomeThing() {
}
}
那這個單例如何來填充屬性呢,增加構造函數和屬性即可啦,請看代碼:
public enum Singleton {
INSTANCE("name", 18);
private String name;
private int age;
Singleton(String name, int age) {
this.name = name;
this.age = age;
}
public void doSomeThing() {
}
}
預設枚舉實例的創建是線程安全的,並且在任何情況下都是單例,上述講的幾種單例模式實現中,有一種情況下他們會重新創建對象,那就是反序列化,將一個單例實例對象寫到磁碟再讀回來,從而獲得了一個實例。反序列化操作提供了readResolve方法,這個方法可以讓開發人員控制對象的反序列化。在上述的幾個方法示例中如果要杜絕單例對象被反序列化是重新生成對象,就必須加入如下方法:
private Object readResolve() throws ObjectStreamException{
return singleton;
}
使用容器實現單例模式
代碼如下:
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<String,Object>();
private Singleton() {
}
public static void registerService(String key, Objectinstance) {
if (!objMap.containsKey(key) ) {
objMap.put(key, instance) ;
}
}
public static ObjectgetService(String key) {
return objMap.get(key) ;
}
}
在程式的初始化,將多個單例類型註入到一個統一管理的類中,使用時通過key來獲取對應類型的對象,這種方式使得我們可以管理多種類型的單例,並且在使用時可以通過統一的介面進行操作。這種方式是利用了Map的key唯一性來保證單例。
CAS實現單例模式
以上實現主要用到了兩點來保證單例,一是JVM的類載入機制,另一個就是加鎖了。那麼有沒有不加鎖的線程安全的單例實現嗎?有點,那就是使用CAS。CAS是項樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變數時,只有其中一個線程能更新變數的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。代碼如下:
public class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
private Singleton() {}
public static Singleton getInstance() {
for (;;) {
Singleton singleton = INSTANCE.get();
if (null != singleton) {
return singleton;
}
singleton = new Singleton();
if (INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
用CAS的好處在於不需要使用傳統的鎖機制來保證線程安全,CAS是一種基於忙等待的演算法,依賴底層硬體的實現,相對於鎖它沒有線程切換和阻塞的額外消耗,可以支持較大的並行度。CAS的一個重要缺點在於如果忙等待一直執行不成功(一直在死迴圈中),會對CPU造成較大的執行開銷。