2010年8月2日 星期一

Singleton

獨體模式 Singleton

確保一個類別只有一個實體,並給它一個存取的全域點(global point)。


有些物件只需要一個實體,如果有多個實體同時存在,反而會造成問題,如 memory pool、garbage collector... 等。

全域物件

最簡單的作法可以採用一個全域物件。但這需要透過程式員之間的約定並嚴格遵守才能達成;並且,這個全域物件在程式開始執行時就建立好了,如果一直都沒用到,反而造成資源無謂地浪費

物件的出生

物件產生 Q&A:

Q: 一個物件如何產生?
A: 對於 C++ 而言,有兩種方式。一種是直接宣告變數式的 Object obj,一種是動態產生 Object* obj = new Object();而對於 Java 而言,只有一種:new Object()。無論哪種方式,都會配置記憶體,然後以 constructor 來初始化物件。

Q: 如何限制人們建立物件?
A: 將 constructors 宣告為 private,如此一來外部無人可以呼叫 constructors。

Q: 那有誰可以建立物件?
A: 物件自己(物件本身的其他方法可以呼叫 constructors)。

Q: 要如何取得一個這樣的物件實體?
A: 物件提供一個方法,稱為 getInstance(),這個方法會初始化一個物件並回傳。

Q: 如果這個實體是一個 static 變數,會怎麼樣?
A: 那所有人都會存取到同一份實體。

獨體模式

獨體模式 Singleton就是可以確保某個類別只有一個實體的方法。將一個類別的 constructors 宣告為 private,並提供一個方法 getInstance() 讓外部取得物件的唯一實體。這個實體是一個 static 物件,若第一次呼叫時這個唯一實體沒有被初始化,則以 private constructor 來為它初始化。如果已經初始化完成了,就直接回傳這個 static 實體。這種在需要時才建立物件的技術,稱為 lazy instantiate

Java 版本程式碼:

JAVA:
  1. public class Singleton
  2. {
  3. private static Singleton uniqueInstance;
  4. private Singleton() {} //private ctor

  5. public static Singleton getInstance() {
  6. if(uniqueInstance == null) {
  7. uniqueInstance = new Singleton();
  8. }
  9. return uniqueInstance;
  10. }
  11. }

C++版本:

C++:
  1. class Singleton
  2. {
  3. Singleton() {} //private ctor

  4. public:
  5. static Singleton& getInstance() {
  6. static Singleton uniqueInstance;
  7. return uniqueInstance;
  8. }
  9. };

這是獨體模式非常簡單的類別圖:
獨體模式

多緒環境的獨體模式

但是上述的獨體模式實踐,在多緒環境下會有 race condition 問題

如 Java 版本第 9 行的判斷式,如果第一個執行緒判定條件成立並進入 if-block,但在產生實體前,就切換到第二個執行緒。則第二個執行緒也會判定條件成立並進入 if-block,造成兩個執行緒各執有一份實體,破壞 Singleton 的唯一性

這種情況在 Java 有三種解決方式。

將 getInstance() 宣告為 synchronized 方法:

JAVA:
  1. public class Singleton
  2. {
  3. private static Singleton uniqueInstance;
  4. private Singleton() {} //private ctor

  5. public static synchoronized Singleton getInstance() { //加上 synchronized 關鍵字
  6. if(uniqueInstance == null) {
  7. uniqueInstance = new Singleton();
  8. }
  9. return uniqueInstance;
  10. }
  11. }

synchronized 關鍵字可以「同步化」一個方法 —— 同時只有一個執行緒可以進入此方法執行。在此執行緒結束之前,其他執行緒只得等待。缺點:每一次呼叫 getInstance() 都得同步化執行(事實上只有第一次需要),效能較差。

「率先」建立實體:

JAVA:
  1. public class Singleton
  2. {
  3. private static Singleton uniqueInstance = new Singleton();

  4. private Singleton() {}

  5. public static Singleton getInstance() {
  6. return uniqueInstance;
  7. }
  8. }

以 static initializer 來建立 uniqueInstance 物件(而不使用 lazy instantiate)。這麼一來 uniqueInstance 在 JVM 載入此類別時就會被建立,無 race condition 問題。缺點:一開始就得建立實體,若不使用則徒然浪費資源

Double-checked lock:

JAVA:
  1. public class Singleton
  2. {
  3. private volatile static Singleton uniqueInstance; //加上 volatile 關鍵字

  4. private Singleton() {}

  5. public static Singleton getInstance() {
  6. if(uniqueInstance == null) { //如果實體不存在,才會進入下面的同步區塊
  7. // ---- synchronized block ---- 同步區塊會 atomic 執行
  8. synchronized(Singleton.class) {
  9. if(uniqueInstance == null) { //再做一次檢查,怕在進入前有其他執行緒趁隙將實體建立
  10. uniqueInstance = new Singleton();
  11. }
  12. }
  13. // ---- synchronized block ----
  14. }
  15. return uniqueInstance;
  16. }
  17. }

volatile 關鍵字 uniqueInstance 宣告為多工處理的共用變數,並以 synchronized block 讓檢查與建立的步驟 atomic 進行。但是加上一個 if 來讓此區塊只有「第一次」才會進入,兼顧了效能。這是效能最好的方案,但較複雜,且 Java 1.4 以前版本不適用



From~http://notes.xamous.net/archives/139

沒有留言:

張貼留言