單例設計模式
- 2.1 孤獨的太陽盤古開天,造日月星辰。
- 2.2 餓漢造日
- 2.3 懶漢的隊伍
- 2.4 大道至簡
讀《秒懂設計模式總結》
單例模式(Singleton)是一種非常簡單且容易理解的設計模式。顧名思義,單例即單一的實例,確切地講就是指在某個系統中只存在一個實例,同時提供集中、統一的訪問接口,以使系統行為保持協調一致。singleton一詞在邏輯學中指“有且僅有一個元素的集合”,這非常恰當地概括了單例的概念,也就是“一個類僅有一個實例”。
2.1 孤獨的太陽盤古開天,造日月星辰。
從“夸父逐日”到“后羿射日”,太陽對于我們的先祖一直具有著神秘的色彩與非凡的意義。隨著科學的不斷發展,我們逐漸揭開了太陽系的神秘面紗。我們可以把太陽系看作一個龐大的系統,其中有各種各樣的對象存在,豐富多彩的實例造就了系統的美好。這個系統里的某些實例是唯一的,如我們賴以生存的恒星太陽。
與其他行星或衛星不同的是,太陽是太陽系內唯一的恒星實例,它持續提供給地球充足的陽光與能量,離開它地球就不會有今天的勃勃生機,但倘若天上有9個太陽,那么將會帶來一場災難。太陽東升西落,循環往復,不多不少僅此一例。
2.2 餓漢造日
既然太陽系里只有一個太陽,我們就需要嚴格把控太陽實例化的過程。我們從最簡單的開始,先來寫一個Sun類。請參看代碼 。
public class Sun {}
太陽類Sun中目前什么都沒有。接下來我們得確保任何人都不能創建太陽的實例,否則一旦程序員調用代碼“new Sun()”,天空就會出現多個太陽,便又需要“后羿”去解決了。有些讀者可能會疑惑,我們并沒有寫構造器,為什么太陽還可以被實例化呢?這是因為Java可以自動為其加上一個無參構造器。為防止太陽實例泛濫將世界再次帶入災難,我們必須禁止外部調用構造器,請參看代碼
public class Sun {private void Sun(){} // 構造器私有化
}
我們在第3行將太陽類Sun的構造方法設為private,使其私有化,如此一來太陽類就被完全封閉了起來,實例化工作完全歸屬于內部事務,任何外部類都無權干預。既然如此,那么我們就讓它自己創建自己,并使其自有永有
public class Sun {private static final Sun sun = new Sun();public Sun() {} // private constructor
}
代碼第3行中“private”關鍵字確保太陽實例的私有性、不可見性和不可訪問性;而“static”關鍵字確保太陽的靜態性,將太陽放入內存里的靜態區,在類加載的時候就初始化了,它與類同在,也就是說它是與類同時期且早于內存堆中的對象實例化的,該實例在內存中永生,內存垃圾收集器(Garbage Collector, GC)也不會對其進行回收;“final”關鍵字則確保這個太陽是常量、恒量,它是一顆終極的恒星,引用一旦被賦值就不能再修改;最后,“new”關鍵字初始化太陽類的靜態實例,并賦予靜態常量sun。這就是“餓漢模式”(eager initialization),即在初始階段就主動進行實例化,并時刻保持一種渴求的狀態,無論此單例是否有人使用。單例的太陽對象寫好了,可一切皆是私有的,外部怎樣才能訪問它呢?正如同程序入口的靜態方法main(),它不需要任何對象引用就能被訪問,我們同樣需要一個靜態方法getInstance()來獲取太陽的單例對象,同時將其設置為“public”以暴露給外部使用
public class Sun {private static final Sun sun = new Sun();public Sun() {} // private constructorpublic static Sun getInstance(){return sun;}
}
太陽單例類的雛形已經完成了,對外部來說只要調用Sun.getInstance()就可以得到太陽對象了,并且不管誰得到,或是得到幾次,得到的都是同一個太陽實例,這樣就確保了整個太陽系中恒星太陽的唯一合法性,他人無法偽造。當然,讀者還可以添加其他功能方法,如發光和發熱等,此處就不再贅述了。
2.3 懶漢的隊伍
至此,我們已經學會了單例模式的“餓漢模式”,讓太陽一開始就準備就緒,隨時供應免費日光。然而,如果始終沒人獲取日光,那豈不是白造了太陽,一塊內存區域被白白地浪費了?這正類似于商家貨品滯銷的情況,貨架上堆放著商品卻沒人買,白白浪費空間。因此,商家為了降低風險,規定有些商品必須提前預訂,這就是“懶漢模式”(lazy initialization)。沿著這個思路,我們繼續對太陽類進行改造
public class Sun {private static Sun sun = new Sun();public Sun() {} // private constructorpublic static Sun getInstance(){if (null == sun) {sun = new Sun(); //沒有sun才構造}return sun;}
}
可以看到我們一開始并沒有造太陽,所以去掉了關鍵字final,只有在某線程第一次調用第9行的getInstance()方法時才會運行對太陽進行實例化的邏輯代碼,之后再請求就直接返回此實例了。這樣的好處是如無請求就不實例化,節省了內存空間;而壞處是第一次請求的時候速度較之前的餓漢初始化模式慢,因為要消耗CPU資源去臨時造這個太陽(即使速度快到可以忽略不計)。這樣的程序邏輯看似沒問題,但其實在多線程模式下是有缺陷的。試想如果是并發請求的話,程序第10行的判空邏輯就會同時成立,這樣就會多次實例化太陽,并且對sun進行多次賦值(覆蓋)操作,這違背了單例的理念。我們再來改良一下,把請求方法加上synchronized(同步鎖)讓其同步,如此一來,某線程調用前必須獲取同步鎖,調用完后會釋放鎖給其他線程用,也就是給請求排隊,一個接一個按順序來
public class Sun {private static Sun sun = new Sun();public Sun() {} // private constructorpublic static synchronized Sun getInstance(){if (null == sun) {sun = new Sun(); //沒有sun才構造}return sun;}
}
我們將太陽類Sun中第9行的getInstance()改成了同步方法,如此可避免多線程陷阱。然而這樣的做法是要付出一定代價的,試想,線程還沒進入方法內部便不管三七二十一直接加鎖排隊,會造成線程阻塞,資源與時間被白白浪費。我們只是為了實例化一個單例對象而已,犯不上如此興師動眾,使用synchronized讓所有請求排隊等候。所以,要保證多線程并發下邏輯的正確性,同步鎖一定要加得恰到好處,其位置是關鍵所在
public class Sun {private volatile static Sun sun = new Sun();public Sun() {} // private constructorpublic static Sun getInstance(){if (null == sun) {synchronized (Sun.class) {if(null == sun){sun = new Sun(); //沒有sun才構造,只有第一次才構造 保證線程安全}}}return sun;}
}
我們在太陽類Sun中第3行對sun變量的定義不再使用find關鍵字,這意味著它不再是常量,而是需要后續賦值的變量;而關鍵字volatile對靜態變量的修飾則能保證變量值在各線程訪問時的同步性、唯一性。需要特別注意的是,對于第9行的getInstance()方法,我們去掉了方法上的關鍵字synchronized,使大家都可以同時進入方法并對其進行開發。請仔細閱讀每行代碼的注釋,有些人(線程)起早就是為了觀看日出,那么這些人會通過第10行的判空邏輯進入觀日臺。而在第11行我們又加上了同步塊以防止多個線程進入,這就類似于觀日臺是一個狹長的走廊,大家排隊進入。隨后在第12行我們又進行一次判空邏輯,這就意味著只有隊伍中的第一個人造了太陽,有幸看到了日出的第一縷陽光,而后面的人則統統離開,直到第17行得到已經造好的太陽
隨后發生的事情我們就可以預見了,太陽高高升起,實例化操作完畢,起晚的人們都無須再進入觀日臺,直接獲取太陽實例就可以了,陽光普照大地,將溫暖灑向人間。大家注意到沒有,我們一共用了2個嵌套的判空邏輯,這就是懶加載模式的“雙檢鎖”:外層放寬入口,保證線程并發的高效性;內層加鎖同步,保證實例化的單次運行。如此里應外合,不僅達到了單例模式的效果,還完美地保證了構建過程的運行效率,一舉兩得。
2.4 大道至簡
相比“懶漢模式”,其實在大多數情況下我們通常會更多地使用“餓漢模式”,原因在于這個單例遲早是要被實例化占用內存的,延遲懶加載的意義并不大,加鎖解鎖反而是一種資源浪費,同步更是會降低CPU的利用率,使用不當的話反而會帶來不必要的風險。越簡單的包容性越強,而越復雜的反而越容易出錯。我們來看單例模式的類結構,如圖2-3所示。單例模式的角色定義如下。
■ Singleton(單例):包含一個自己的類實例的屬性,并把構造方法用private關鍵字隱藏起來,對外只提供getInstance()方法以獲得這個單例對象。
除了“餓漢”與“懶漢”這2種單例模式,其實還有其他的實現方式。但萬變不離其宗,它們統統都是由這2種模式發展、衍生而來的。我們都知道Spring框架中的IoC容器很好地幫我們托管了業務對象,如此我們就不必再親自動手去實例化這些對象了,而在默認情況下我們使用的正是框架提供的“單例模式”。誠然,究其代碼實現當然不止如此簡單,但我們應該追本溯源,抓住其本質的部分,理解其核心的設計思想,再針對不同的應用場景做出相應的調整與變動,結合實踐舉一反三。