設計模式-單例模式

單例模式

1)提供了對唯一實例的受控訪問。
2)由於在系統內存中只存在一個對象,因此可以節約系統資源,對於一些需要頻繁創建和銷燬的對象單例模式無疑可以提高系統的性能。
3)允許可變數目的實例。

特點

1)單例類只能有一個實例。
2)單例類必須自己自己創建自己的唯一實例。
3)單例類必須給所有其他對象提供這一實例

此片文章部分實例爲轉載

餓漢模式

public class SingletonClass { 
  private static final SingletonClass instance = new SingletonClass(); 
  public static SingletonClass getInstance() { 
    return instance; 
  } 
  private SingletonClass() { 
  } 
}

但是有一個問題——無論這個類是否被使用,都會創建一個instance對象。

懶漢模式

public class Singleton {
    private static Singleton uniqueInstance = null;
    private Singleton() {
       // Exists only to defeat instantiation.
    }
    public static Singleton getInstance() {
       if (uniqueInstance == null) {
           uniqueInstance = new Singleton();
       }
       return uniqueInstance;
    }
    // Other methods...
}

但是該方法線程並不安全,可能多線程間併發調用。
線程A希望使用SingletonClass,調用getInstance()方法。因爲是第一次調用,A就發現instance是null的,於是它開始創建實例,就在這個時候,CPU發生時間片切換,線程B開始執行,它要使用SingletonClass,調用getInstance()方法,同樣檢測到instance是null——注意,這是在A檢測完之後切換的,也就是說A並沒有來得及創建對象——因此B開始創建。B創建完成後,切換到A繼續執行,因爲它已經檢測完了,所以A不會再檢測一遍,它會直接創建對象。這樣,線程A和B各自擁有一個SingletonClass的對象——單例失敗!

登記式單例類

public class SingletonClass { 
  private static SingletonClass instance = null; 
    
  public synchronized static SingletonClass getInstance() { 
    if(instance == null) { 
      instance = new SingletonClass(); 
    } 
    return instance; 
  } 
  private SingletonClass() { 
  } 
}
getInstance()加上同步鎖,一個線程必須等待另外一個線程創建完成後才能使用這個方法,這就保證了單例的唯一性。然而,簡單的東西往往不夠理想。這段代碼毫無疑問存在性能的問題——synchronized修飾的同步塊可是要比一般的代碼段慢上幾倍的!如果存在很多次getInstance()的調用,那性能問題就不得不考慮了!

優化

public class SingletonClass { 
  private static SingletonClass instance = null; 
  public static SingletonClass getInstance() { 
    if (instance == null) { 
      synchronized (SingletonClass.class) { 
        if (instance == null) { 
          instance = new SingletonClass(); 
        } 
      } 
    } 
    return instance; 
  } 
  private SingletonClass() { 
  }
}

首先判斷instance是不是爲null,如果爲null,加鎖初始化;如果不爲null,直接返回instance。這就是double-checked locking設計實現單例模式。到此爲止,一切都很完美。我們用一種很聰明的方式實現了單例模式。

出錯

下面我們來考慮這麼一種情況:線程A開始創建SingletonClass的實例,此時線程B調用了getInstance()方法,首先判斷instance是否爲null。按照我們上面所說的內存模型,A已經把instance指向了那塊內存,只是還沒有調用構造方法,因此B檢測到instance不爲null,於是直接把instance返回了——問題出現了,儘管instance不爲null,但它並沒有構造完成,就像一套房子已經給了你鑰匙,但你並不能住進去,因爲裏面還沒有收拾。此時,如果B在A將instance構造完成之前就是用了這個實例,程序就會出現錯誤了!

public class SingletonClass { 

  private static SingletonClass instance = null; 

  public static SingletonClass getInstance() { 
    if (instance == null) { 
      SingletonClass sc; 
      synchronized (SingletonClass.class) { 
        sc = instance; 
        if (sc == null) { 
          synchronized (SingletonClass.class) { 
            if(sc == null) { 
              sc = new SingletonClass(); 
            } 
          } 
          instance = sc; 
        } 
      } 
    } 
    return instance; 
  } 

  private SingletonClass() { 

  } 
    
}

我們在第一個同步塊裏面創建一個臨時變量,然後使用這個臨時變量進行對象的創建,並且在最後把instance指針臨時變量的內存空間。寫出這種代碼基於以下思想,即synchronized會起到一個代碼屏蔽的作用,同步塊裏面的代碼和外部的代碼沒有聯繫。因此,在外部的同步塊裏面對臨時變量sc進行操作並不影響instance,所以外部類在instance=sc;之前檢測instance的時候,結果instance依然是null。
不過,這種想法完全是錯誤的!同步塊的釋放保證在此之前——也就是同步塊裏面——的操作必須完成,但是並不保證同步塊之後的操作不能因編譯器優化而調換到同步塊結束之前進行。因此,編譯器完全可以把instance=sc;這句移到內部同步塊裏面執行。這樣,程序又是錯誤的了!

JAVA中的單例模式

說了這麼多,難道單例沒有辦法在Java中實現嗎?其實不然!
在JDK 5之後,Java使用了新的內存模型。volatile關鍵字有了明確的語義——在JDK1.5之前,volatile是個關鍵字,但是並沒有明確的規定其用途——被volatile修飾的寫變量不能和之前的讀寫代碼調整,讀變量不能和之後的讀寫代碼調整!因此,只要我們簡單的把instance加上volatile關鍵字就可以了。
public class SingletonClass { 

  private volatile static SingletonClass instance = null; 

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

  private SingletonClass() { 

  } 
    
} 

然而,這只是JDK1.5之後的Java的解決方案,那之前版本呢?其實,還有另外的一種解決方案,並不會受到Java版本的影響:
 
public class SingletonClass { 
    
  private static class SingletonClassInstance { 
    private static final SingletonClass instance = new SingletonClass(); 
  } 

  public static SingletonClass getInstance() { 
    return SingletonClassInstance.instance; 
  } 

  private SingletonClass() { 

  } 
    
} 


在這一版本的單例模式實現代碼中,我們使用了Java的靜態內部類。這一技術是被JVM明確說明了的,因此不存在任何二義性。在這段代碼中,因爲SingletonClass沒有static的屬性,因此並不會被初始化。直到調用getInstance()的時候,會首先加載SingletonClassInstance類,這個類有一個static的SingletonClass實例,因此需要調用SingletonClass的構造方法,然後getInstance()將把這個內部類的instance返回給使用者。由於這個instance是static的,因此並不會構造多次。
 
由於SingletonClassInstance是私有靜態內部類,所以不會被其他類知道,同樣,static語義也要求不會有多個實例存在。並且,JSL規範定義,類的構造必須是原子性的,非併發的,因此不需要加同步塊。同樣,由於這個構造是併發的,所以getInstance()也並不需要加同步。
 
至此,我們完整的瞭解了單例模式在Java語言中的時候,提出了兩種解決方案。個人偏向於第二種,並且Effiective Java也推薦的這種方式。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章