Head First 設計模式 —— 05. 單例模式

全局變數的缺點

如果將對象賦值給一個全局變數,那麼必須在程式一開始就創建好對象 P170

  • 和 JVM 實現有關,有些 JVM 的實現是:在用到的時候才創建對象

思考題

Choc-O-Holic 公司使用如下工業強度巧克力鍋爐控制器

public class ChocolateBoiler {
    private boolean empty;
    private boolean boiled;
    
    public ChocolateBoiler() {
        empty = true;
        boiled = false;
    }
    
    public void fill() {
        if (isEmpty()) {
            empty = false;
            boiled = false;
            // 在鍋爐內填滿巧克力和牛奶的混合物
        }
    }
    
    public void drain() {
        if (!isEmpty() && isBoiled()) {
            // 排出煮沸的巧克力和牛奶
            empty = true;
        }
    }
    
    public void boil() {
        if (!isEmpty() && ! isBoiled()) {
            // 將爐內物煮沸
            boiled = true;
        }
    }
    
    public boolean isEmpty() {
        return empty;
    }
    
    public boolnea isBoiled() {
        return boiled;
    }
}

思考題

Choc-O-Holic 公司在有意識地防止不好的事情發生,你不這麼認為嗎?你可能會擔心,如果同時存在兩個 ChocolateBoiler(巧克力鍋爐)實例,可能將會發生很糟糕的事情。
萬一同時有多於一個的 ChocolateBoiler(巧克力鍋爐)實例存在,可能發生哪些很糟糕的事呢? P176

  • 由於只有一個物理世界的鍋爐,所以如果存在多個實例時,不同實例內的變數可能與物理世界的鍋爐情況不對應,造從而成錯誤的操作。
    • 多執行緒初始化了兩個實例 a 和 b,a 先成功進行 fill() 操作,此時 b 也準備進行 fill() 操作,但由於 b 內的變數沒有與物理世界的鍋爐情況對應,所以 b 也可以進行 fill() 操作,導致了原料溢出。
  • 剛開始怎麼也想不到會出現什麼問題,看了後面單例模式多執行緒的問題後,仔細思考了一下,只能想到上述可能性。當然,書中忽略了只有一個實例時也存在多執行緒並發錯誤的問題(一定程度導致難以想到上述可能性)。

單例模式

確保一個類只有一個實例,並提供一個全局訪問點。 P177
單例模式

  • Java 1.2 之前,垃圾收集器有個 bug,單例沒有全局的引用時會被當作垃圾清楚。Java 1.2 及以後不存在上述問題。 P184

思考題

所有變數和方法都定義為靜態的,直接把類當作一個單例,這樣如何? P184

  • 靜態初始化的控制權在 Java 手上,這樣做可能導致混亂,特別是當有許多類牽涉其中時。

思考題

多個類載入器有機會創建各自的單例實例,如何避免? P184

  • 自行指定類載入器,並指定同一個類載入器。

單例模式的七種方法

推薦使用靜態內部類和枚舉方式

餓漢式 P181
public class Singleton {
    private final static Singleton INSTANCE = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return INSTANCE;
    }
}
  • 特點
    • 執行緒安全
    • 依賴 JVM 類載入機制:JVM 在載入這個類時會馬上創建唯一的單例實例 P181
  • 缺點
    • 與全局變數一樣:必須在程式一開始就實例化,沒有懶載入 P170
餓漢式(變種)
public class Singleton {
    private static Singleton INSTANCE;
    
    static {
        INSTANCE = new Singleton();
    }
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

【擴展】 靜態程式碼塊初始化靜態變數最好放在定義變數之後,否則會在執行定義變數可能出現被覆蓋的問題(如果定義有賦值(包括 null),則會覆蓋靜態程式碼塊已賦的值)。
原因:靜態域的初始化和靜態程式碼塊的執行會從上到下依次執行。
如下寫法最終會得到 null

public class Singleton {
    static {
        INSTANCE = new Singleton();
    }
    
    private static Singleton INSTANCE = null;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return INSTANCE;
    }
}
懶漢式 P176
public class Singleton {
    private static Singleton INSTANCE = null;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}
  • 特點
    • 使用時再實例化
  • 缺點
    • 執行緒不安全
懶漢式(變種) P180
public class Singleton {
    private static Singleton INSTANCE = null;
    
    private Singleton() {}
    
    public static synchronized Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}
  • 特點
    • 執行緒安全
    • 使用時再實例化
  • 缺點
    • 效率低
雙重校驗鎖 P182
public class Singleton {
    private volatile static Singleton INSTANCE = null;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}
  • 特點
    • 執行緒安全
    • 使用時再實例化
    • 效率較高
    • volatile 關鍵字確保:當 INSTANCE 變數杯初始化成 Singleton 實例時,多個執行緒能正確地處理 INSTANCE 變數 P182
    • 1.4及更早版本會失效,1.5及以後版本適用 P182
靜態內部類
public class Singleton {
    private static class SingletonHolder {
        private final static Singleton INSTANCE = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
  • 特點
    • 執行緒安全
    • 使用時再實例化
    • 依賴 JVM 類載入機制:開始只有 Singleton 被載入了,只有在主動使用 SingletonHolder 時(即調用 getInstance() 時),才會載入 SingletonHolder 類,從而實例化 INSTANCE
枚舉
public enum Singleton {
    INSTANCE
}
  • 特點
    • 執行緒安全
    • 克隆、反射和反序列化均不會破壞單例(上述六種方式都會被破壞)
    • 程式碼簡單
    • 1.5及以後版本才有枚舉
    • 初始化就會實例化(反編譯後可以發現寫法類似餓漢式(變種)

本文首發於公眾號:滿賦諸機(點擊查看原文) 開源在 GitHub :reading-notes/head-first-design-patterns