在軟體開發中,常常會遇到需要確保某個類別(Class)只有一個執行個體(instance)存在的情況。這時候,就可以使用「單一執行個體模式」(Singleton Pattern)來實現這個需求。單一執行個體模式保證了在整個應用程式中,只有一個 Class 的執行個體存在,並提供一個全域的存取媒介供外部存取這個唯一的執行個體。在這篇文章中,我們將會使用 Java 作為示範來介紹幾種實作單一執行個體模式的方式,並探討它們的優缺點。
# 基本 Singleton 模式
這是一個簡單又直觀的 Singleton 範例:
class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
如果在程式中不會使用到多執行緒,那以上這樣的寫法並沒有太大的問題。我們透過向外部提供一個可用來取得 instance 的方法 getInstance()
,並用 if (instance == null)
判斷存不存在來阻止重複建立 instance 的可能,只有為 null
的時候才會 new Singleton()
,否則會直接傳回 instance。
但在多執行緒程式的情況下就不一樣了。當多個執行緒同時進入 getInstance()
且 instance 為 null
時,它們都會進入條件判斷的區塊。
- 執行緒 A 在進入條件判斷後,暫停執行,然後執行緒 B 也進入條件判斷。
- 執行緒 B 在進入條件判斷後,同樣認為 instance 為
null
,因此建立了一個新的 Singleton 物件並將其指派給 instance。 - 執行緒 B 結束後,執行緒 A 恢復執行。由於執行緒 A 被暫停時並不知道 instance 已經被執行緒 B 建立,它將建立另一個 Singleton 物件並將其指派給 instance。
結果是,我們最終有兩個 Singleton 物件,這違反了 Singleton 設計模式的目標。
# 同步處理 Synchronization
為了解決這個問題,可以使用同步處理(synchronization)來確保在同一時間只有一個執行緒可以進入 getInstance()
。以下是修改後的程式碼範例:
class Singleton {
private static Singleton instance;
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
這樣做可以確保當一個執行緒進入 getInstance()
時,其他執行緒必須等待,直到該執行緒離開為止。這樣可以確保只有一個 Singleton 物件被建立。
儘管這種修改可以解決多執行緒環境下的競爭問題,但它會對效能造成一些影響。每次呼叫 getInstance()
時都需要進行同步化,這可能會在高併發的情況下導致效能瓶頸。因此,如果應用程式在高併發環境中執行且 Singleton 的建立成本相對較低,則可以考慮使用「雙重檢查鎖定」(Double-Checked Locking)等技巧。
# 雙重檢查鎖定 Double-Checked Locking
我們可以不必讓它每次都鎖起來,只要在 instance 還沒被建立的時候再鎖就好了。
class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null){
synchronized(Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
為什麼內層還要再判斷一次 instance 是不是 null
呢?因為可能發生以下情況:
- 有兩個執行緒「同時」呼叫
getInstance()
想要建立 instance。 - 既然是同時,他們都可以通過第一層
if
條件的檢查。 - 因為我們有上鎖,假設執行緒 A 進入,執行緒 B 在排隊。
- 執行緒 A 建立了 instance 後離開,但是執行緒 B 完全不知情所以又建立了一個 instance。
此時如果有第二道 if
條件判斷式,就不會發生執行緒 B 還繼續再建立 instance 的狀況了。
然後還有一點值得注意的是 volatile
。如果在 instance 變數宣告時不使用 volatile
,則可能發生以下情況:
- 當一個執行緒 A 建立 Singleton 物件時,其他執行緒可能無法立即看到 instance 的變更。
- 其他執行緒 B 可能會繼續進入
getInstance()
,並在 instance 仍然為null
的情況下誤以為 Singleton 物件尚未建立。 - 執行緒 B 可能會取得一個未完全初始化的 Singleton 物件,這可能導致未定義的行為或錯誤。
這種未定義的行為稱為「指令重排」(Instruction Reordering),即在多執行緒環境中,編譯器或處理器可能會重新排序指令以提高效能。這可能導致 instance 的初始化在其他操作之前發生。
# 靜態初始化
class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
因為在 Class 載入時就會立即建立 Singleton 物件並將其指派給 instance。這種方式稱為「餓漢式」(Eager Initialization),它確保在任何執行緒存取 getInstance()
之前,已經有一個唯一的 Singleton 物件存在。由於 instance 是在靜態初始化階段建立的,因此在多執行緒環境中不需要額外的同步化處理。
然而,值得注意的是,這種餓漢式的實作方式可能會在 class 載入時就建立 Singleton 物件而提前佔用系統資源,而不論是否真的需要它。這可能會導致不必要的資源浪費,尤其在 Singleton 物件的建立成本較高時。因此,這種實作方式在某些情況下可能不是最佳選擇。
不過,在大多數情況下,使用餓漢型的 Singleton 模式就夠用了,等到真正有效能考慮時再改寫吧!
# 完整範例
// Test.java
public class Test {
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
if (instance1 == instance2) {
System.out.println("兩個執行個體相同");
} else {
System.out.println("兩個執行個體不同");
}
}
}
class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
// Write your code here
}
public static Singleton getInstance() {
return instance;
}
}
編譯並執行
$ javac Test.java && java Test