單例模式深入學習1
單例模式的定義是保證一個類僅有一個例項,並提供一個全域性訪問點。一般用在工具類、應用配置、資料庫連線池的建立上。
優點是一個類在記憶體裡只有一個例項,減少記憶體開銷,可以避免對資源的多重佔用。
缺點是沒有藉口,無法擴充套件。
單例模式的重點:私有構造器、執行緒安全、延遲載入、序列化和序列化安全、防止反射攻擊。
單例模式分為餓漢模式和懶漢模式。簡單的餓漢模式和懶漢模式的建立如圖。

餓漢模式

懶漢模式
餓漢模式是在類被載入時例項就已經被建立。若系統從始至終都未呼叫這個例項,則會造成資源浪費,再或者這個類的例項初始化比較佔用資源,多個類都在載入時建立例項就會造成系統服務啟動慢。因此可以使用延遲載入,即初次呼叫時再初始化例項。注意的是兩種模式都需要將構造器私有化。
執行緒安全
而上圖所示懶漢模式是執行緒不安全的,如果只有一個執行緒呼叫這個類,的確只會初始化一個類。但如果多執行緒同時獲取例項,當兩個執行緒同時到達第10行,即判斷lazySingleton==null的時候,此時例項都未初始化,兩個執行緒同時判斷為true,同時進入11行,去初始化例項,就會在過程中這個物件new了兩次。現在寫一個測試方法。

當直接執行時,看到兩個執行緒獲取到的都是一個例項。

現在,在懶漢模式的第10行判斷例項是否為空除打上斷點,並設定斷點為執行緒生效。

debug執行測試main函式,兩個執行緒都執行到第10行,都單步除錯到第11行,此時兩個執行緒都執行結束。可以看到debug干預下能復現可能出現的問題:過程中存在兩個例項。

如何消除這個隱患?
synchronized給方法加鎖
第一種方式是在獲取例項的方法加上關鍵字synchronized,給方法加鎖,讓方法變成同步方法。而此處獲取獲取例項的方法是靜態方法,則鎖住的是這個類。多執行緒時,一個執行緒進入這個方法時,其他執行緒就無法進入,處於等待狀態,鎖釋放後才能進入。這樣能確保多個執行緒同時只有一個執行緒能初始化物件。

在第10行打上斷點,再次除錯,當一個執行緒進入後,選擇另一個執行緒,會有以下提示。

通過這種同步的方法解決了懶漢模式的執行緒安全問題,但是同步鎖本身比較消耗資源,有加鎖和解鎖的開銷。而且synchronized載入static方法上,鎖住的是類,範圍比較大,對效能有影響。
doublecheck
第二種方式是將鎖加在方法內部,進行雙重判斷。即便兩個執行緒同時判斷例項為空,但接下來只有一個程序會進入鎖,並在此判斷此時例項是否為空,空則初始化物件。這樣是鎖的範圍縮小,降低synchronized的效能開銷。

雙重校驗懶漢模式
這種寫法看上去很完美,但是仍然存在隱患,問題是處在第10行和第13行。當一個執行緒進到第13行時,new了一個物件。雖然看上去是一步,實際上new一個物件經歷了3個步驟。
1.分配記憶體給這個物件
2.初始化物件
3.設定lazyDoubleCheckSingleton(instance)指向剛分配的記憶體地址
而第二步和第三步可能會被重排序。即先分配記憶體給物件,再將instanc指向剛分配的地址,此時物件還未初始化完成,另一個程序在第10行進行空判斷的時候判斷lazyDoubleCheckSingleton不為空,結果return回去的仍然是空。
java語言規範中說,所有程式在執行java程式時,必須遵守intra-thread semantics這個規定,允許哪些保證重排序對於單執行緒不會改變程式執行結果的重排序,從而提高執行效能。
解決重排序帶來的隱患
一種解決辦法是禁止這種重排序,做法就是宣告instance的時候加上volatile關鍵字。通過volatile和doublecheck的這個方法既兼顧了效能又兼顧了執行緒安全。對於這個欄位的理解可以參考: ofollow,noindex">https://www.cnblogs.com/zhengbin/p/5654805.html

而另一種方法,不禁止重排序,而是基於靜態內部類初始化的解決方案。即StaticInnerClassSingleton的instance是在靜態內部類中被初始化的。而靜態內部類InnerClass本身被載入時jvm會給這個內部類上鎖,而staticInnerClassSingleton在初始化賦值的過程中即使發生重排序,其他執行緒也無法獲取例項。只有InnerClass被載入完成後,其他執行緒才能訪問。這種方式實現了延遲載入,降低建立例項帶來的開銷,也能兼顧執行緒安全。
