Head First 設計模式 —— 05. 單例模式
阿新 • • 發佈:2021-01-09
全域性變數的缺點
如果將物件賦值給一個全域性變數,那麼必須在程式一開始就建立好物件 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