前言 Singleton設計模式,確保全局只存在一個該類的實例。將構造器聲明為private,防止調用(雖然還是可以使用反射來調用)。聲明一個靜態的類實例在類中,聲明一個公共的獲取實例的方法。這篇博文給出了簡單的實現方法,分析如何做到線程安全,整理了使用Singleton的壞處。 線程安全 方法一是 ...
前言
Singleton設計模式,確保全局只存在一個該類的實例。將構造器聲明為private,防止調用(雖然還是可以使用反射來調用)。聲明一個靜態的類實例在類中,聲明一個公共的獲取實例的方法。這篇博文給出了簡單的實現方法,分析如何做到線程安全,整理了使用Singleton的壞處。
線程安全
方法一是線程安全的,在類被裝載的時候,就初始化這個成員,Java庫中Runtime就是用了這個方法。
方法二不是線程安全的。如果多個線程同時進入到函數中(臨界區),那麼會返回多個不同的實例。
代碼驗證
以下代碼驗證了線程不安全,即多線程的情況下方法二不能保證真正的“單例”。通過列印每個類的hashCode來顯示,類實例不唯一。
class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
try {
Thread.sleep(233);
singleton = new Singleton();
}
catch (Exception e) {
e.printStackTrace();
}
}
return singleton;
}
}
public class Main {
public static void main(String args[]) {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton.hashCode());
}
}).start();
}
}
}
解決方案
一種簡單的處理方法是,將getInstance聲明為synchronized的,效率低,因為每一個線程都在等待進入臨界區。
另一種方法叫做Double Checked Locking(DCL),將公共變數聲明為volatile(每次要使用這個變數的時候,從記憶體中取出來,保證各個線程可以看到這個變數的修改)。
class Singleton {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
try {
Thread.sleep(233);
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
catch (Exception e) {
e.printStackTrace();
}
}
return singleton;
}
}
壞處
有啥壞處,別怪這個設計模式,都是全局變數的錯。
1, 全局變數
Singleton管理公共的資源,在代碼中的任何地方,只要調用Singleton的獲取實例的方法,就可以獲取到Singleton的實例,可以對這些公共資源的讀寫。這讓Singleton成為了一個全局變數。
全局變數的壞處,也是Singleton的壞處。全局變數自有全局變數的使用情景和優點,要分開兩面看待,這裡只講壞處並非想說全局變數一無是處。
在分析全局變數的壞處之前,在[2]下麵看到了一個特別有意思的比喻:
Using global state with your fellow programmers is like using the same toothbrush with your friends - you can but you never know when someone will decide to shove it up his bum.
我想這位老兄說出這個觀點,大概是用過異味的牙刷吧。為何不用規範來約束一同使用全局變數的人呢?
這位老兄道出了全局變數的本質,任何人可以用它做任何事,給整個程式帶來了極大的不確定性。網上關於全局變數的壞處討論很多,下麵整理了一些,雖然並非都很壞:
- 代碼耦合程度更高。假設一些函數依賴於全局變數的狀態,這些函數通過這個全局變數聯繫到一起。一個函數的修改對另一個函數的讀取存在影響,這些函數在無形中聯繫到了一起。
- 給測試帶來困難。全局變數存儲了一些狀態,需要安排好模塊的運行順序才可以正確的測試。可是,單元測試應該是相互獨立的,而非有順序的。
- 多線程的寫入的時候,要互斥。
- 破壞了函數的輸入輸出功能。拿全局變數來讀輸入,寫輸出。
- 命名衝突。
- 可讀性降低。要理解一個函數,還需要去跟蹤使用到的全局變數的來龍去脈。
2, 破壞了單一職責原則
定義:就一個類而言,應該僅有一個引起它變化的原因
當一個類使用了Singleton的時候,它不僅負責這個類需要完成的任務,還負責這個單一對象資源的創建和管理。這個類的函數做兩項任務,相關性低。
這一點算不上太壞。畢竟設計原則並不總是需要遵守的。
正確使用
上面講了壞處,核心要義是避免將Singleton用作全局變數。Singleton的使用場景是什麼呢?
回到Singleton的本質作用上來,只需要一個這個類的實例。前面講這個設計模式破壞了單一職責原則,因為需要管理這個實例。
所以歸結起來使用這個設計模式的兩個原因:
(1) 單個實例
(2) 實例的管理
在[1]中講了使用Singleton的一個具體例子。那就是日誌(log)。因為log和代碼的實質功能並不會產生耦合,所以是否開啟log對於系統的功能沒有太大的影響。
參考鏈接
- https://stackoverflow.com/questions/228164/on-design-patterns-when-should-i-use-the-singleton
- https://softwareengineering.stackexchange.com/questions/148108/why-is-global-state-so-evil
- https://stackoverflow.com/questions/26285520/implementing-singleton-with-an-enum-in-java
- https://stackoverflow.com/questions/137975/what-is-so-bad-about-singletons?page=1&tab=votes#tab-top