第2章 單例

單例模式(Singleton)是一種非常簡單且容易理解的設計模式。顧名思義,單例即單一的實例,確切地講就是指在某個系統中只存在一個實例,同時提供集中、統一的訪問接口,以使系統行為保持協調一致。singleton一詞在邏輯學中指“有且僅有一個元素的集合”,這非常恰當地概括了單例的概念,也就是“一個類僅有一個實例”。
2.1 孤獨的太陽
盤古開天,造日月星辰。從“夸父逐日”到“后羿射日”,太陽對于我們的先祖一直具有著神秘的色彩與非凡的意義。隨著科學的不斷發展,我們逐漸揭開了太陽系的神秘面紗。我們可以把太陽系看作一個龐大的系統,其中有各種各樣的對象存在,豐富多彩的實例造就了系統的美好。這個系統里的某些實例是唯一的,如我們賴以生存的恒星太陽,如圖2-1所示。

圖2-1 太陽系
與其他行星或衛星不同的是,太陽是太陽系內唯一的恒星實例,它持續提供給地球充足的陽光與能量,離開它地球就不會有今天的勃勃生機,但倘若天上有9個太陽,那么將會帶來一場災難。太陽東升西落,循環往復,不多不少僅此一例。
2.2 餓漢造日
既然太陽系里只有一個太陽,我們就需要嚴格把控太陽實例化的過程。我們從最簡單的開始,先來寫一個Sun類。請參看代碼清單2-1。
代碼清單2-1 太陽類Sun
1. public class Sun {
2.
3. }
如代碼清單2-1所示,太陽類Sun中目前什么都沒有。接下來我們得確保任何人都不能創建太陽的實例,否則一旦程序員調用代碼“new Sun()”,天空就會出現多個太陽,便又需要“后羿”去解決了。有些讀者可能會疑惑,我們并沒有寫構造器,為什么太陽還可以被實例化呢?這是因為Java可以自動為其加上一個無參構造器。為防止太陽實例泛濫將世界再次帶入災難,我們必須禁止外部調用構造器,請參看代碼清單2-2。
代碼清單2-2 太陽類Sun
1. public class Sun {
2.
3. private Sun(){//構造方法私有化
4.
5. }
6.
7. }
如代碼清單2-2所示,我們在第3行將太陽類Sun的構造方法設為private,使其私有化,如此一來太陽類就被完全封閉了起來,實例化工作完全歸屬于內部事務,任何外部類都無權干預。既然如此,那么我們就讓它自己創建自己,并使其自有永有,請參看代碼清單2-3。
代碼清單2-3 太陽類Sun
1. public class Sun {
2.
3. private static final Sun sun = new Sun();//自有永有的單例
4.
5. private Sun(){//構造方法私有化
6.
7. }
8.
9. }
如代碼清單2-3所示,代碼第3行中“private”關鍵字確保太陽實例的私有性、不可見性和不可訪問性;而“static”關鍵字確保太陽的靜態性,將太陽放入內存里的靜態區,在類加載的時候就初始化了,它與類同在,也就是說它是與類同時期且早于內存堆中的對象實例化的,該實例在內存中永生,內存垃圾收集器(Garbage Collector,GC)也不會對其進行回收;“final”關鍵字則確保這個太陽是常量、恒量,它是一顆終極的恒星,引用一旦被賦值就不能再修改;最后,“new”關鍵字初始化太陽類的靜態實例,并賦予靜態常量sun。這就是“餓漢模式”(eager initialization),即在初始階段就主動進行實例化,并時刻保持一種渴求的狀態,無論此單例是否有人使用。
單例的太陽對象寫好了,可一切皆是私有的,外部怎樣才能訪問它呢?正如同程序入口的靜態方法main(),它不需要任何對象引用就能被訪問,我們同樣需要一個靜態方法getInstance()來獲取太陽的單例對象,同時將其設置為“public”以暴露給外部使用,請參看代碼清單2-4。
代碼清單2-4 太陽類Sun
1. public class Sun {
2.
3. private static final Sun sun = new Sun();//自有永有的太陽單例
4.
5. private Sun(){//構造方法私有化
6.
7. }
8.
9. public static Sun getInstance(){//陽光普照,方法公開化
10. return sun;
11. }
12.
13. }
如代碼清單2-4所示,太陽單例類的雛形已經完成了,對外部來說只要調用Sun.getInstance()就可以得到太陽對象了,并且不管誰得到,或是得到幾次,得到的都是同一個太陽實例,這樣就確保了整個太陽系中恒星太陽的唯一合法性,他人無法偽造。當然,讀者還可以添加其他功能方法,如發光和發熱等,此處就不再贅述了。
2.3 懶漢的隊伍
至此,我們已經學會了單例模式的“餓漢模式”,讓太陽一開始就準備就緒,隨時供應免費日光。然而,如果始終沒人獲取日光,那豈不是白造了太陽,一塊內存區域被白白地浪費了?這正類似于商家貨品滯銷的情況,貨架上堆放著商品卻沒人買,白白浪費空間。因此,商家為了降低風險,規定有些商品必須提前預訂,這就是“懶漢模式”(lazy initialization)。沿著這個思路,我們繼續對太陽類進行改造,請參看代碼清單2-5。
代碼清單2-5 太陽類Sun
1. public class Sun {
2.
3. private static Sun sun;//這里不進行實例化
4.
5. private Sun(){//構造方法私有化
6.
7. }
8.
9. public static Sun getInstance() {
10. if (sun == null) {//如果無日才造日
11. sun = new Sun();
12. }
13. return sun;
14. }
15.
16. }
如代碼清單2-5所示,可以看到我們一開始并沒有造太陽,所以去掉了關鍵字final,只有在某線程第一次調用第9行的getInstance()方法時才會運行對太陽進行實例化的邏輯代碼,之后再請求就直接返回此實例了。這樣的好處是如無請求就不實例化,節省了內存空間;而壞處是第一次請求的時候速度較之前的餓漢初始化模式慢,因為要消耗CPU資源去臨時造這個太陽(即使速度快到可以忽略不計)。
這樣的程序邏輯看似沒問題,但其實在多線程模式下是有缺陷的。試想如果是并發請求的話,程序第10行的判空邏輯就會同時成立,這樣就會多次實例化太陽,并且對sun進行多次賦值(覆蓋)操作,這違背了單例的理念。我們再來改良一下,把請求方法加上synchronized(同步鎖)讓其同步,如此一來,某線程調用前必須獲取同步鎖,調用完后會釋放鎖給其他線程用,也就是給請求排隊,一個接一個按順序來,請參看代碼清單2-6。
代碼清單2-6 太陽類Sun
1. public class Sun {
2.
3. private static Sun sun;//這里不進行實例化
4.
5. private Sun(){//構造方法私有化
6.
7. }
8.
9. public static synchronized Sun getInstance() {//此處加入同步鎖
10. if (sun == null) {//如果無日才造日
11. sun = new Sun();
12. }
13. return sun;
14. }
15.
16. }
如代碼清單2-6所示,我們將太陽類Sun中第9行的getInstance()改成了同步方法,如此可避免多線程陷阱。然而這樣的做法是要付出一定代價的,試想,線程還沒進入方法內部便不管三七二十一直接加鎖排隊,會造成線程阻塞,資源與時間被白白浪費。我們只是為了實例化一個單例對象而已,犯不上如此興師動眾,使用synchronized讓所有請求排隊等候。所以,要保證多線程并發下邏輯的正確性,同步鎖一定要加得恰到好處,其位置是關鍵所在,請參看代碼清單2-7。
代碼清單2-7 太陽類Sun
1. public class Sun {
2.
3. private volatile static Sun sun;
4.
5. private Sun(){//構造方法私有化
6.
7. }
8.
9. public static Sun getInstance() {//華山入口
10. if (sun == null) {//觀日臺入口
11. synchronized(Sun.class){//觀日者進行排隊
12. if (sun == null) {
13. sun = new Sun();//只有排頭兵造了太陽,旭日東升
14. }
15. }
16. }
17. return sun; //……陽光普照,其余人不必再造日
18. }
19. }
如代碼清單2-7所示,我們在太陽類Sun中第3行對sun變量的定義不再使用find關鍵字,這意味著它不再是常量,而是需要后續賦值的變量;而關鍵字volatile對靜態變量的修飾則能保證變量值在各線程訪問時的同步性、唯一性。需要特別注意的是,對于第9行的getInstance()方法,我們去掉了方法上的關鍵字synchronized,使大家都可以同時進入方法并對其進行開發。請仔細閱讀每行代碼的注釋,有些人(線程)起早就是為了觀看日出,那么這些人會通過第10行的判空邏輯進入觀日臺。而在第11行我們又加上了同步塊以防止多個線程進入,這就類似于觀日臺是一個狹長的走廊,大家排隊進入。隨后在第12行我們又進行一次判空邏輯,這就意味著只有隊伍中的第一個人造了太陽,有幸看到了日出的第一縷陽光,而后面的人則統統離開,直到第17行得到已經造好的太陽,如圖2-2所示。

圖2-2 觀日臺
隨后發生的事情我們就可以預見了,太陽高高升起,實例化操作完畢,起晚的人們都無須再進入觀日臺,直接獲取太陽實例就可以了,陽光普照大地,將溫暖灑向人間。
大家注意到沒有,我們一共用了2個嵌套的判空邏輯,這就是懶加載模式的“雙檢鎖”:外層放寬入口,保證線程并發的高效性;內層加鎖同步,保證實例化的單次運行。如此里應外合,不僅達到了單例模式的效果,還完美地保證了構建過程的運行效率,一舉兩得。
2.4 大道至簡
相比“懶漢模式”,其實在大多數情況下我們通常會更多地使用“餓漢模式”,原因在于這個單例遲早是要被實例化占用內存的,延遲懶加載的意義并不大,加鎖解鎖反而是一種資源浪費,同步更是會降低CPU的利用率,使用不當的話反而會帶來不必要的風險。越簡單的包容性越強,而越復雜的反而越容易出錯。我們來看單例模式的類結構,如圖2-3所示。單例模式的角色定義如下。

圖2-3 單例模式的類結構
- Singleton(單例):包含一個自己的類實例的屬性,并把構造方法用private關鍵字隱藏起來,對外只提供getInstance()方法以獲得這個單例對象。
除了“餓漢”與“懶漢”這2種單例模式,其實還有其他的實現方式。但萬變不離其宗,它們統統都是由這2種模式發展、衍生而來的。我們都知道Spring框架中的IoC容器很好地幫我們托管了業務對象,如此我們就不必再親自動手去實例化這些對象了,而在默認情況下我們使用的正是框架提供的“單例模式”。誠然,究其代碼實現當然不止如此簡單,但我們應該追本溯源,抓住其本質的部分,理解其核心的設計思想,再針對不同的應用場景做出相應的調整與變動,結合實踐舉一反三。