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 來載入單件類,這樣就有效避免了上述問題
六。總結
應用單件模式可以保證物件的唯一性,但要注意單件模式的適用範圍。不應該濫用單件模式,因為畢竟需要管理的資源敏感物件不會很多
暫無評論,快來發表你的看法吧