Head First 設計模式 —— 05. 單例模式
- 2021 年 1 月 8 日
- 筆記
- Head First 設計模式, 設計模式
全局變量的缺點
如果將對象賦值給一個全局變量,那麼必須在程序一開始就創建好對象 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()
操作,導致了原料溢出。
- 多線程初始化了兩個實例 a 和 b,a 先成功進行
剛開始怎麼也想不到會出現什麼問題,看了後面單例模式多線程的問題後,仔細思考了一下,只能想到上述可能性。當然,書中忽略了只有一個實例時也存在多線程並發錯誤的問題(一定程度導致難以想到上述可能性)。
單例模式
確保一個類只有一個實例,並提供一個全局訪問點。 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