1. 程式人生 > >設計模式之單件模式

設計模式之單件模式

單件模式確保一個類只有一個例項,並提供一個全域性訪問點。

有一些物件我們只需要一個,比方說:執行緒池、快取、對話方塊、處理器偏好設定和登錄檔的物件等等。事實上,這類物件只能有一個例項,如果製造出多個例項,就會導致許多問題產生,例如:程式的行為異常、資源使用過量,或者是不一致的結果。

使用靜態變數

如何確保這些類只存在一個例項?利用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()的時間耗費。