設計模式之單件模式
單件模式確保一個類只有一個例項,並提供一個全域性訪問點。
有一些物件我們只需要一個,比方說:執行緒池、快取、對話方塊、處理器偏好設定和登錄檔的物件等等。事實上,這類物件只能有一個例項,如果製造出多個例項,就會導致許多問題產生,例如:程式的行為異常、資源使用過量,或者是不一致的結果。
使用靜態變數
如何確保這些類只存在一個例項?利用java的靜態變數可以做到,但使用靜態變數有個缺點:如果將物件賦值給一個全域性變數,那麼你必須在程式一開始就建立好物件。萬一這個物件非常耗費資源,而程式在這次的執行過程中又一直沒用到它,就形成了浪費。
經典的單件實現
以下是經典的單件實現:
public class Singleton { // 利用一個靜態變數來記錄Singleton的唯一例項。 private static Singleton uniqueInstance; // 把構造器宣告為私有的,只有Singleton類內才可以呼叫構造器。 private Singleton() { } // 用getInstance()方法例項化物件,並返回這個例項。 public static Singleton getInstance() { // 如果uniqueInstance是空的,表示還沒有建立例項。 if (uniqueInstance == null) { // 如果uniqueInstance是空的,我們就利用私有的構造器產生一個Singleton例項並 // 把它賦值給uniqueInstance靜態變數中。請注意,如果我們不需要這個例項,它就 // 永遠不會產生。這就是“延遲例項化” uniqueInstance = new Singleton(); } return uniqueInstance; } }
getInstance()是靜態的,這意味著它是一個類方法,所以可以在程式碼的任何地方使用Singleton.getInstance()訪問它。這和訪問全域性變數一樣簡單,只是多了個優點:單件可以延遲例項化。
處理多執行緒
假如有兩個執行緒同時呼叫Singleton.getInstance(),而這時uniqueInstance還沒有初始化,那麼有可能會出現呼叫Singleton.getInstance()方法返回不同的例項。
只要把getInstance()變成同步方法,多執行緒災難幾乎就可以輕易地解決了:
public class Singleton { private static Singleton uniqueInstance; private Singleton() { } // 通過增加synchronized關鍵字到getInstance()方法中,我們 // 迫使每個執行緒在進入這個方法之前,要先等候別的執行緒離開該方法。 // 也就是說,不會有兩個執行緒可以同時進入這個方法。 public static synchronized Singleton getInstance() { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } }
上面的程式碼可以解決多執行緒的問題,但是同步會降低效能,這不又是另一個問題嗎?
只有第一次執行此方法時,才真正需要同步,一旦設定好uniqueInstance變數,就不需要同步這個方法了。之後每次呼叫這個方法,同步都是一種累贅。
改善多執行緒
為了要符合大多數java應用程式,很明顯的,我們需要確保單件模式能在多執行緒的狀況下正常工作。但是似乎同步getInstance()的做法將拖垮效能,該怎麼辦你?
可以有一些選擇:
1. 如果getInstance()的效能對應用程式不是很關鍵,就什麼都別
沒錯,如果你的應用程式可以接受getInstance()造成的額外負擔,就忘了這件事吧。同步getInstance()的方法既簡單又有效。但是你必須知道,同步一個方法可能造成程式效率下降100倍。因此如果將getInstance()的程式使用在頻繁執行的地方,你可能就要重新考慮了。
2. 使用“急切”建立例項,而不用延遲例項化的做法
如果應用程式總是建立並使用單件例項,或者在建立和執行時方面的負擔不太繁重,你可能想要急切建立此單件,如下所示:
public class Singleton {
// 在靜態初始化器中建立單件。這段程式碼保證了執行緒安全
private static Singleton uniqueInstance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
// 已經有例項了,直接使用它
return uniqueInstance;
}
}
利用這個做法,我們依賴JVM在載入這個類時馬上建立此唯一的單件例項。JVM保證在任何執行緒訪問uniqueInstance靜態變數之前,一定先建立此例項。
3. 用“雙重檢查加鎖”,在getInstance()中減少使用同步
利用雙重檢查加鎖,首先檢查是否例項已經建立了,如果尚未建立,才進行同步。這樣一來,只有第一次會同步,這正是我們想要的。
public class Singleton {
// volatile關鍵詞確保,當uniqueInstance變數被初始化成Singleton例項時,
// 多個執行緒正確地處理uniqueInstance變數
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
// 檢查例項,如果不存在,就進入同步區塊。
if (uniqueInstance == null) {
// 注意,只有第一次才徹底執行這裡的程式碼
synchronized (Singleton.class){
// 進入區塊後,再檢查一次。如果仍是null,才建立例項
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
如果效能是你關心的重點,那麼這個做法可以幫你大大地減少getInstance()的時間耗費。