設計模式(01) 單例模式(建立類模式)(下,懶漢模式和雙重檢查鎖)
From Now On,Let us begin Design Patterns。
懶漢模式和雙重檢查鎖
這篇文章我們接著上一篇文章,繼續設計模式裡面的單例模式:這一篇我們要寫的是懶漢模式和雙重檢查加鎖的例項,我用我個人的程式設計經驗跟大家講述這個很有趣的故事,而且您聽完會覺得很簡單。
說出我的故事:懶漢模式
剛剛工作的時候,我老大分配給我一個任務:寫一個跟fastdfs(一個開源的輕量級分散式檔案系統,它對檔案進行管理,功能包括:檔案儲存、檔案同步、檔案訪問(檔案上傳、檔案下載)等,解決了大容量儲存和負載均衡的問題。特別適合以檔案為載體的線上服務,如相簿網站、視訊網站等等。)的底層系統,我們會用到公司的自己封裝的一些jar包。
專案過程中,我需要一個分發請求的類,這個類用於分發所有的儲存請求,同時我想實現的是,這個類只能一個,否則無法輪換分發了,所以我的單例是這麼實現的:
不完整的懶漢模式:
我要一個單例,所以我就直接寫了:如下:
是不是很簡單,當時我就是這麼寫的,哈哈。後來我一看,不對呀!!!!我的建構函式的訪問許可權是public的,要是我不小心new多了一個或者別人new了呢????所以,我規定只能用private作為建構函式,於是乎,就有了下面的版本:
終於大公告成了吧!!我很有成就感呀,當時我還沒看過設計模式,彷彿這就是自己發明得的,老高興了。
後來我測試專案(公司內部使用),我發現居然沒有辦法獲得單例,這可把我急壞了,一看,媽的原來少了同步關鍵字:synchronized,於是乎:
完整的懶漢模式:
事情過了一個星期,我老大看我的程式碼,看到我使用這個單例模式的時候,先是對我肯定了一次,然後提了兩點中肯的意見,第一:使用了這種模式的地方命名要規範,方便他人閱讀;第二:這個同步方法影響的區域太大,如果多個物件想獲取這個物件的時候會柱塞在這排隊,建議縮小同步區域。
於是,才有了下文
不完整的雙重檢查鎖:
老大要我將同步塊的範圍縮小,好說好說,那我就將同步塊的大小縮小一下吧:於是,我站在單執行緒的角度出發,就有了下面的程式碼:
後來用的時候發現有時候行有時候不行,雖然程式碼的思路沒錯,但是這樣的實現肯定是有問題的,於是乎我分析了一下,問題如下:
當 instance 為 null 時,兩個執行緒可以併發地進入 if 語句內部。然後,一個執行緒進入 synchronized 塊來初始化 singleton ,而另一個執行緒則被阻斷。當第一個執行緒退出 synchronized 塊時,等待著的執行緒進入並建立另一個 Singleton 物件。注意:當第二個執行緒進入 synchronized 塊時,它並沒有檢查 instance 是否非 null。
為處理上面的問題,我們需要對 singleton 進行第二次檢查。這就是“雙重檢查鎖定”名稱的由來。
所以我的程式碼就變成了下面的模樣:
雙重檢查鎖定背後的理論是:在 //2 處的第二次檢查使(如上上例子 中那樣)建立兩個不同的 Singleton 物件成為不可能。假設有下列事件序列:
1. 執行緒 1 進入 getInstance() 方法。
2. 由於 singleton為 null,執行緒 1 在 //1 處進入 synchronized 塊。
3. 執行緒 1 被執行緒 2 預佔。
4. 執行緒 2 進入 getInstance() 方法。
5. 由於 singleton仍舊為 null,執行緒 2 試圖獲取 //1 處的鎖。然而,由於執行緒 1 持有該鎖,執行緒 2 在 //1 處阻塞。
6. 執行緒 2 被執行緒 1 預佔。
7. 執行緒 1 執行,由於在 //2 處例項仍舊為 null,執行緒 1 還建立一個 Singleton 物件並將其引用賦值給 singleton。
8. 執行緒 1 退出 synchronized 塊並從 getInstance() 方法返回例項。
9. 執行緒 1 被執行緒 2 預佔。
10. 執行緒 2 獲取 //1 處的鎖並檢查 instance 是否為 null。
11. 由於 singleton是非 null 的,並沒有建立第二個 Singleton 物件,由執行緒 1 建立的物件被返回。
這麼一分析,我就很滿意啦,這個就能實現單例了吧,當時我是深信不疑的,直到後來。。。。。。
完整的雙重檢查鎖:
後來呀後來,我發現這個單例也是時候時壞的,夠嗆,我這次努力查資料,算是徹底弄明白了,資料是這麼說的:
雙重檢查鎖定背後的理論是完美的。不幸地是,現實完全不同。雙重檢查鎖定的問題是:並不能保證它會在單處理器或多處理器計算機上順利執行。
雙重檢查鎖定失敗的問題並不歸咎於 JVM 中的實現 bug,而是歸咎於 Java 平臺記憶體模型。記憶體模型允許所謂的“無序寫入”,這也是這些習語失敗的一個主要原因。
無序寫入帶來的問題:
為解釋該問題,需要重新考察上例子中的 //3 行。此行程式碼建立了一個 Singleton 物件並初始化變數 singleton 來引用此物件。這行程式碼的問題是:在 Singleton 建構函式體執行之前,變數 instance 可能成為非 null 的。
什麼?這一說法可能讓您始料未及,但事實確實如此。在解釋這個現象如何發生前,請先暫時接受這一事實,我們先來考察一下雙重檢查鎖定是如何被破壞的。假設上例 中程式碼執行以下事件序列:
1.執行緒 1 進入 getInstance() 方法。
2.由於 singleton 為 null,執行緒 1 在 //1 處進入 synchronized 塊。
3.執行緒 1 前進到 //3 處,但在建構函式執行之前,使例項成為非 null。
4.執行緒 1 被執行緒 2 預佔。
5.執行緒 2 檢查例項是否為 null。因為例項不為 null,執行緒 2 將 singleton引用返回給一個構造完整但部分初始化了的 Singleton物件。
6.執行緒 2 被執行緒 1 預佔。
7.執行緒 1 通過執行 Singleton 物件的建構函式並將引用返回給它,來完成對該物件的初始化。
此事件序列發生線上程 2 返回一個尚未執行建構函式的物件的時候。
為展示此事件的發生情況,假設為程式碼行 instance =new Singleton(); 執行了下列虛擬碼: instance =new Singleton();
mem = allocate(); //Allocate memory for Singleton object.
instance = mem; //Note that instance is now non-null, but
//has not been initialized.
ctorSingleton(instance); //Invoke constructor for Singleton passing
//instance.
為了解決這個無序寫入的問題:我們可以用volatile保證寫入順序的一致性:
完整的雙重檢查鎖程式碼如下:
到這裡,總算完成了懶漢模式和雙重檢查鎖的程式碼啦。其實綜合來看:我們的選擇順序可以是:靜態內部類 > 餓漢模式 > 雙重檢查鎖 > 懶漢模式