跳到主要內容
黯羽輕揚每天積累一點點

設計模式之單件模式(Singleton Pattern)

免費2015-03-06#Design_Pattern#单件模式#单例模式#Singleton_Pattern

單件模式又被稱為單例模式,定義很簡單——只允許存在指定類的唯一實例(instance),並由該類提供全域訪問點。但在實際應用的時候會遇到很多問題,比如在多執行緒,或多個 classloader 環境下,如何保證單件模式的正確性等等

no_mkd

一。單件模式是什麼?

單件模式也被稱為單例模式,它的作用說白了就是為了確保“該類的實例只有一個”

單件模式經常被用來管理資源敏感的物件,比如:資料庫連接物件、註冊表物件、執行緒池物件等等,這種物件如果同時存在多個的話就會造成各種不一致的麻煩(你總不希望發生資料庫重複連接的異常吧)

二。如何保證類的實例只有一個?

(這個問題看似簡單,但如果沒有接觸過單件模式的話,要想出來解決方案還是需要一些天賦的。。不信的話,可以試著想想。。)

1.類的實例可能只有一個嗎?貌似只要知道類名就可以隨便 new 了吧?
當然可以,知道類名的話,確實可以呼叫其建構方法來 new 實例,但是,注意一點:這個類必須要有公開的建構方法才能從外部 new 實例,不是嗎?

2.那就是說要保證類的實例只有一個的話,這個類不能有公開的建構方法,對吧?
沒錯,就是這樣,我們需要定義一個私有的建構方法

3.一個沒有公開建構方法的類能夠產生實例嗎?如果建構方法是 private,那麼只有該類的實例才能呼叫這個建構方法,同樣的要呼叫這個建構方法才能產生該類的實例。。這不是“雞生蛋,蛋生雞。。”的問題嗎?用私有的建構方法當然可以生產實例,上面忽略了一點:並不是“只有該類的實例才能呼叫這個建構方法”。因為在該類內部就可以隨便呼叫這個私有的建構方法,並不需要建立任何實例

有了上面的討論結果,我們就可以實現經典的單件模式了:

package SingletonPattern;

/**

  • @author ayqy

  • 最經典的單件模式 */ public class Singleton {

    private static Singleton instance;//定義靜態實例變數

    /**

    • 定義私有建構方法,防止從外部 new 實例 */ private Singleton(){ //初始化操作 }

    /**

    • 提供全域訪問點
    • @return 該類的實例 */ public static Singleton getInstance(){ if(instance == null) instance = new Singleton(); return instance; }

    /*

    • 其它有用的屬性和行為
    • 畢竟應用了單件模式的類仍然具有原本的功能
    • */ }

注意:一定要清楚最後一點,應用了單件模式的類並不應該喪失其原本的功能,千萬不能為了使用而使用

三。繼續思考我們的單件模式

我們的單件模式已經萬無一失了嗎?不,它還存在很多問題,比如:

1.多執行緒環境下;2.多個 class loader 環境下

我們無法保證產生的實例只有一個,對吧?

但是作為一種成熟的設計模式,單件模式必須要能從容應對這些環境,所以,接下來我們將討論如何應對這些環境

四。多執行緒環境下的單件模式

如何在多執行緒環境下保證實例的唯一性?很容易想到用 synchronized 關鍵字來保證執行緒安全,就像這樣:

/**
 * 提供全域訪問點
 * @return 該類的實例
 */
public static synchronized Singleton getInstance(){
	if(instance == null)
		instance = new Singleton();
	return instance;
}

我們把 getInstance 方法定義為同步方法就保證了不會有多個執行緒同時進入該方法,就不會產生不同的實例了

但是上面的方法存在致命的問題:用 synchronized 關鍵字同步方法會極大的降低效率(同步一個方法甚至可能造成百倍的效率下降。。),這會拖垮我們的程式。有什麼好的改進方法呢?

首先,上面的同步塊是整個 getIntance 方法,每次��叫該方法都會強制進入同步機制,但仔細一想,我們只在第一此呼叫該方法時需要進行同步(第一次 new 物件),之後的呼叫直接返回 new 好的物件就好了

那麼,我們的改進方案就是:用雙重加鎖實現只在第一次 new 物件時進行同步

package SingletonPattern;

/**

  • @author ayqy

  • 多執行緒下的單件模式 2——利用雙重加鎖保證只在實例化變數的時候進行同步 */ public class DoubleLockSingleton {

    private static volatile DoubleLockSingleton instance;//定義靜態實例變數

    /**

    • 定義私有建構方法,防止從外部 new 實例 */ private DoubleLockSingleton(){ //初始化操作 }

    /**

    • 提供全域訪問點
    • @return 該類的實例 */ public static DoubleLockSingleton getInstance(){ if(instance == null) synchronized(DoubleLockSingleton.class){//進入同步塊 if(instance == null)//再次判空 instance = new DoubleLockSingleton(); } return instance; }

    /*

    • 其它有用的屬性和行為
    • 畢竟應用了單件模式的類仍然具有原本的功能
    • */ }

注意:雙重加鎖體現在 volatile 關鍵字(告訴編譯器,這個變數不能被保留副本,一旦發生變動就會強制寫回,避免了不一致)和 synchronized 修飾的同步塊

但要明白這樣做的代價,volatile 關鍵字也會告訴編譯器,不要對該物件進行編譯優化。只看第一次 new 物件的過程的話,雙重加鎖的效率甚至要比同步方法更低,但在雙重加鎖方式在以後的呼叫中不再需要進行同步,所以長遠看來雙重加鎖的效率要高於同步方法

有沒有一種方法不需要使用龜速的同步機制就能保證執行緒安全呢?如果有的話,絕對能夠大大提高效率,對吧?

當然有,這種方法叫做“急切初始化”(順便提一下,開篇提到的“經典單件模式”其實用了“延遲初始化”的方法。。很簡單,不必解釋),一起看看吧:

package SingletonPattern;

/**

  • @author ayqy

  • 多執行緒環境下的單件模式——用“急切初始化”來保證執行緒安全 */ public class EagerlyInitSingleton {

    private static EagerlyInitSingleton instance = new EagerlyInitSingleton();//定義靜態實例變數,並在類載入的時候就進行初始化操作

    /**

    • 定義私有建構方法,防止從外部 new 實例 */ private EagerlyInitSingleton(){ //初始化操作 }

    /**

    • 提供全域訪問點
    • @return 該類的實例 */ public static synchronized EagerlyInitSingleton getInstance(){ return instance; }

    /*

    • 其它有用的屬性和行為
    • 畢竟應用了單件模式的類仍然具有原本的功能
    • */ }

額,這也能叫方法嗎?這種方法貌似不合標準吧?沒關係,這種方法自然有它的優點,比如:

1.效率很高,且執行緒安全;2.簡單易用,什麼都不用考慮,甚至不用判斷

但其致命的缺點是:資源浪費問題,如果這個物件是一個巨大的極其耗費資源的物件,而我們在一開始就建立了它,卻遲遲沒有用到,這將是非常傷的。。

上面提到了三種保證執行緒同步的方式,如何選擇必須要結合具體情況來定,應綜合考慮效率,資源利用等各個因素

五。多個 class loader 環境下的單件模式

如果存在多個類載入器,多個類載入器可能同時載入我們的單件類,從而產生多個實例

對於這種情況,我們可以顯式指定使用哪一個 class loader 來載入單件類,這樣就有效避免了上述問題

六。總結

應用單件模式可以保證物件的唯一性,但要注意單件模式的適用範圍。不應該濫用單件模式,因為畢竟需要管理的資源敏感物件不會很多

評論

暫無評論,快來發表你的看法吧

提交評論