深入淺出設計模式(一):單例模式
注:本文參考《深入淺出設計模式》和網上資料,並對某些文字以自己的理解進行了適當的修改。個人覺得本文應作為入門學習,瞭解大體框架,具體的設計模式有待詳細研究。
1. 單一指責原則(SRP,Single Responsibility Principle)
系統裡的每一個物件都應該只有一個單獨的職責,而所有物件所關注的就是自身職責的完成。每個類應該只有一個職責,對外只能提供一種功能,而引起類變化的原因應該只有一個。2. 開閉原則(OCP,Open Close Principle)
對擴充套件開放,對修改關閉。在程式需要進行拓展的時候,不能去修改原有的程式碼,實現一個熱插拔的效果。所以一句話概括就是:為了使程式的擴充套件性好,易於維護和升級。想要達到這樣的效果,我們需要使用介面和抽象類。3. 依賴注入原則(DIP,Dependence Inversion Principle)
面對介面程式設計,依賴於抽象而不依賴於具體。4. 里氏替換原則(LSP,Liskov Substitution Principle)
任何基類可以出現的地方,子類一定可以出現。 LSP是繼承複用的基石,只有當衍生類可以替換掉基類,軟體單位的功能不受到影響時,基類才能真正被複用,而衍生類也能夠在基類的基礎上增加新的行為。LSP是對“開-閉”原則的補充。實現“開-閉”原則的關鍵步驟就是抽象化。而基類與子類的繼承關係就是抽象化的具體實現,所以里氏代換原則是對實現抽象化的具體步驟的規範。5. 迪米特法則(最少知道原則)(DP,Demeter Principle)(LOD,Law of Demeter)
一個物件應當儘量少的與其他物件之間發生相互作用,使得系統功能模組相對獨立(降低各個物件之間的耦合),提高系統可維護性。6. 介面隔離原則(ISP,Interface Segregation Principle)
使用多個隔離的介面,比使用單個介面要好。不應該強迫客戶程式依賴它們不需要使用的方法。(一個介面應該只提供一種對外功能,不應該把所有的操作都封裝到一個介面當中)7.優先使用組合而不是繼承原則(CARP,Composite/Aggregate Reuse Principle)
1. 建立型模式,共五種
單例模式(Singleton)
簡單工廠模式(Simple Factory),此模式並不在23種設計模式之中
工廠方法模式(Factory Method)
抽象工廠模式(Abstract Factory)
原型模式(Prototype)
建立者模式(Builder)
2. 結構型模式,共七種
介面卡模式(Adapter)
門面模式(Facade)
代理模式(Proxy)
合成模式(Composite)
享元模式(Flyweight)
裝飾模式(Decorator)
橋接模式(Bridge)
3. 行為型模式,共11種
策略模式(Strategy)
迭代器模式(Iterator)
模版方法模式(Template Method)
中介者模式(Mediator)
訪問者模式(Vistor)
職責鏈模式(Chain of Responsibility)
狀態模式(State)
直譯器模式(Interpreter)
觀察者模式(Observer)
命令模式(Command)
備忘錄模式(Memento)
單例模式在java中的Runtime類、資料庫連線池和日誌管理中典型應用,總結起來就是當程式執行時,需要保證一個物件只有一個例項存在時,就應該用到單例模式。這樣的模式有幾個好處:
某些類建立比較頻繁,對於一些大型的物件,這是一筆很大的系統開銷。
省去了new操作符,降低了系統記憶體的使用頻率,減輕GC壓力。
有些類如交易所的核心交易引擎,控制著交易流程,如果該類可以建立多個的話,系統完全亂了。(比如一個軍隊出現了多個司令員同時指揮,肯定會亂成一團),所以只有使用單例模式,才能保證核心交易伺服器獨立控制整個流程。
package singleton;
public class Singleton {
private static Singleton instance = new Singleton();
/* 私有構造方法,防止被例項化 */
private Singleton() {
}
/* 靜態工程方法,建立例項 */
public static Singleton getInstance() {
return instance;
}
}
public class Singleton {
/* 持有私有靜態例項,防止被引用,此處賦值為null,目的是實現延遲載入 */
private static Singleton instance = null;
/* 私有構造方法,防止被例項化 */
private Singleton() {
}
/* 靜態工程方法,建立例項 */
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
/* 如果該物件被用於序列化,可以保證物件在序列化前後保持一致 */
public Object readResolve() {
return instance;
}
}
此懶漢模式在單執行緒的程式應用中是沒有任何問題的,但是在多執行緒程式中就會出現問題(當多個執行緒都進行if(instance == null)判斷時,就會產生多個該類的示例)。我們首先會想到對getInstance方法加synchronized關鍵字,如下:
// 增加同步機制
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
但是因為這種寫法是將synchronized機制放在了獲取例項的方法上,導致程式每取一次例項,都將進入synchronized機制,效率低。
事實上,只有在第一次建立物件的時候需要加鎖,之後就不需要了,Double-checked locking(雙檢測鎖)機制:
public static Singleton getInstance() {
if (instance == null) {
// 同步機制放在產生例項的程式碼前
synchronized (instance) {
if (instance == null) { //對是否為null再次判斷
instance = new Singleton();
}
}
}
return instance;
}
但是,這樣的情況,還是有可能有問題的,看下面的情況:
在Java指令中建立物件和賦值操作是分開進行的,也就是說instance = new Singleton();語句是分兩步執行的。但是JVM並不保證這兩個操作的先後順序,也就是說有可能JVM會為新的Singleton例項分配空間,然後直接賦值給instance成員,然後再去初始化這個Singleton例項。這樣就可能出錯了。
我們以A、B兩個執行緒為例:
- A、B執行緒同時進入了第一個if判斷
- A首先進入synchronized塊,由於instance為null,所以它執行instance = new Singleton();
- 由於JVM內部的優化機制,JVM先畫出了一些分配給Singleton例項的空白記憶體,並賦值給instance成員(注意此時JVM沒有開始初始化這個例項),然後A離開了synchronized塊。
- B進入synchronized塊,由於instance此時不是null,因此它馬上離開了synchronized塊並將結果返回給呼叫該方法的程式。
- 此時B執行緒打算使用Singleton例項,卻發現它沒有被初始化,於是錯誤發生了。
所以程式還是有可能發生錯誤,其實程式在執行過程是很複雜的,從這點我們就可以看出,尤其是在寫多執行緒環境下的程式更有難度,有挑戰性。我們對該程式做進一步優化:
public class Singleton {
/* 私有構造方法,防止被例項化 */
private Singleton() {
}
/* 此處使用一個內部類來維護單例 */
private static class SingletonFactory {
private static Singleton instance = new Singleton();
}
/* 獲取例項 */
public static Singleton getInstance() {
return SingletonFactory.instance;
}
/* 如果該物件被用於序列化,可以保證物件在序列化前後保持一致 */
public Object readResolve() {
return getInstance();
}
}
其實說它完美,也不一定,如果在建構函式中丟擲異常,例項將永遠得不到建立,也會出錯。所以說,十分完美的東西是沒有的,我們只能根據實際情況,選擇最適合自己應用場景的實現方法。也有人這樣實現:因為我們只需要在建立類的時候進行同步,所以只要將建立和getInstance()分開,單獨為建立加synchronized關鍵字,也是可以的:
public class Singleton {
private static Singleton instance = null;
private Singleton () {
}
private static synchronized void syncInit() {
if (instance == null) {
instance = new Singleton ();
}
}
public static Singleton getInstance() {
if (instance == null) {
syncInit();
}
return instance;
}
}
首先,靜態類不能實現介面。(從類的角度說是可以的,但是那樣就破壞了靜態了。因為介面中不允許有static修飾的方法,所以即使實現了也是非靜態的)
其次,單例可以被延遲初始化,靜態類一般在第一次載入是初始化。之所以延遲載入,是因為有些類比較龐大,所以延遲載入有助於提升效能。
再次,單例類可以被繼承,他的方法可以被覆寫。但是靜態類內部方法都是static,無法被覆寫。
最後一點,單例類比較靈活,畢竟從實現上只是一個普通的Java類,只要滿足單例的基本需求,你可以在裡面隨心所欲的實現一些其它功能,但是靜態類不行。
從上面這些概括中,基本可以看出二者的區別,但是,從另一方面講,我們上面最後實現的那個單例模式,內部就是用一個靜態類來實現的,所以,二者有很大的關聯,只是我們考慮問題的層面不同罷了。兩種思想的結合,才能造就出完美的解決方案,就像HashMap採用陣列+連結串列來實現一樣,其實生活中很多事情都是這樣,單用不同的方法來處理問題,總是有優點也有缺點,最完美的方法是,結合各個方法的優點,才能最好的解決問題!