如何正確地寫出單例模式
轉自:http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/
單例模式算是設計模式中最容易理解,也是最容易手寫程式碼的模式了吧。但是其中的坑卻不少,所以也常作為面試題來考。本文主要對幾種單例寫法的整理,並分析其優缺點。很多都是一些老生常談的問題,但如果你不知道如何建立一個執行緒安全的單例,不知道什麼是雙檢鎖,那這篇文章可能會幫助到你。
懶漢式,執行緒不安全
當被問到要實現一個單例模式時,很多人的第一反應是寫出如下的程式碼,包括教科書上也是這樣教我們的。
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
|
這段程式碼簡單明瞭,而且使用了懶載入模式,但是卻存在致命的問題。當有多個執行緒並行呼叫 getInstance() 的時候,就會建立多個例項。也就是說在多執行緒下不能正常工作。
懶漢式,執行緒安全
為了解決上面的問題,最簡單的方法是將整個 getInstance() 方法設為同步(synchronized)。
public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }
|
雖然做到了執行緒安全,並且解決了多例項的問題,但是它並不高效。因為在任何時候只能有一個執行緒呼叫 getInstance() 方法。但是同步操作只需要在第一次呼叫時才被需要,即第一次建立單例例項物件時。這就引出了雙重檢驗鎖。
雙重檢驗鎖
雙重檢驗鎖模式(double checked locking pattern),是一種使用同步塊加鎖的方法。程式設計師稱其為雙重檢查鎖,因為會有兩次檢查 instance == null
,一次是在同步塊外,一次是在同步塊內。為什麼在同步塊內還要再檢驗一次?因為可能會有多個執行緒一起進入同步塊外的
if,如果在同步塊內不進行二次檢驗的話就會生成多個例項了。
public static Singleton getSingleton() { if (instance == null) { //Single Checked synchronized (Singleton.class) { if (instance == null) { //Double Checked instance = new Singleton(); } } } return instance ; }
|
這段程式碼看起來很完美,很可惜,它是有問題。主要在於instance = new Singleton()
這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。
- 給 instance 分配記憶體
- 呼叫 Singleton 的建構函式來初始化成員變數
- 將instance物件指向分配的記憶體空間(執行完這步 instance 就為非 null 了)
但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被執行緒二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以執行緒二會直接返回 instance,然後使用,然後順理成章地報錯。
我們只需要將 instance 變數宣告成 volatile 就可以了。
public class Singleton { private volatile static Singleton instance; //宣告成 volatile private Singleton (){} public static Singleton getSingleton() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
|
有些人認為使用 volatile 的原因是可見性,也就是可以保證執行緒在本地不會存有 instance 的副本,每次都是去主記憶體中讀取。但其實是不對的。使用 volatile 的主要原因是其另一個特性:禁止指令重排序優化。也就是說,在 volatile 變數的賦值操作後面會有一個記憶體屏障(生成的彙編程式碼上),讀操作不會被重排序到記憶體屏障之前。比如上面的例子,取操作必須在執行完 1-2-3 之後或者 1-3-2 之後,不存在執行到 1-3 然後取到值的情況。從「先行發生原則」的角度理解的話,就是對於一個 volatile 變數的寫操作都先行發生於後面對這個變數的讀操作(這裡的“後面”是時間上的先後順序)。
但是特別注意在 Java 5 以前的版本使用了 volatile 的雙檢鎖還是有問題的。其原因是 Java 5 以前的 JMM (Java 記憶體模型)是存在缺陷的,即時將變數宣告成 volatile 也不能完全避免重排序,主要是 volatile 變數前後的程式碼仍然存在重排序問題。這個 volatile 遮蔽重排序的問題在 Java 5 中才得以修復,所以在這之後才可以放心使用 volatile。
相信你不會喜歡這種複雜又隱含問題的方式,當然我們有更好的實現執行緒安全的單例模式的辦法。
餓漢式 static final field
這種方法非常簡單,因為單例的例項被宣告成 static 和 final 變數了,在第一次載入類到記憶體中時就會初始化,所以建立例項本身是執行緒安全的。
public class Singleton{ //類載入時就初始化 private static final Singleton instance = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return instance; } }
|
這種寫法如果完美的話,就沒必要在囉嗦那麼多雙檢鎖的問題了。缺點是它不是一種懶載入模式(lazy initialization),單例會在載入類後一開始就被初始化,即使客戶端沒有呼叫 getInstance()方法。餓漢式的建立方式在一些場景中將無法使用:譬如 Singleton 例項的建立是依賴引數或者配置檔案的,在 getInstance() 之前必須呼叫某個方法設定引數給它,那樣這種單例寫法就無法使用了。
靜態內部類 static nested class
我比較傾向於使用靜態內部類的方法,這種方法也是《Effective Java》上所推薦的。
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
|
這種寫法仍然使用JVM本身機制保證了執行緒安全問題;由於 SingletonHolder 是私有的,除了 getInstance() 之外沒有辦法訪問它,因此它是懶漢式的;同時讀取例項的時候不會進行同步,沒有效能缺陷;也不依賴 JDK 版本。
列舉 Enum
用列舉寫單例實在太簡單了!這也是它最大的優點。下面這段程式碼就是宣告列舉例項的通常做法。
public enum EasySingleton{ INSTANCE; }
|
我們可以通過EasySingleton.INSTANCE來訪問例項,這比呼叫getInstance()方法簡單多了。建立列舉預設就是執行緒安全的,所以不需要擔心double checked locking,而且還能防止反序列化導致重新建立新的物件。但是還是很少看到有人這樣寫,可能是因為不太熟悉吧。
總結
一般來說,單例模式有五種寫法:懶漢、餓漢、雙重檢驗鎖、靜態內部類、列舉。上述所說都是執行緒安全的實現,文章開頭給出的第一種方法不算正確的寫法。
就我個人而言,一般情況下直接使用餓漢式就好了,如果明確要求要懶載入(lazy initialization)會傾向於使用靜態內部類,如果涉及到反序列化建立物件時會試著使用列舉的方式來實現單例。
Read More
相關文章
- 單例模式的正確寫法單例模式
- Java 如何正確地輸出日誌Java
- 如何寫出一個好的單例模式單例模式
- 多方位全面解析:如何正確地寫好一個介面
- Javascript 正確地引用表單和表單元素JavaScript
- Android-如何正確地使用HandlerAndroid
- 如何寫出正確的二分法以及分析
- 單例模式 - 確定 N 先生的GrilFriend單例模式
- 如何理解單例模式?單例模式
- 面試之---手寫單例模式面試單例模式
- 單例模式(下)---聊一聊單例模式的幾種寫法單例模式
- 單例模式(下) - 聊一聊單例模式的幾種寫法單例模式
- 單例模式(下) – 聊一聊單例模式的幾種寫法單例模式
- 【譯】如何通過 INUIAddVoiceShortcutButtonDelegate 正確地使用 INUIAddVoiceShortcutButtonUI
- 單例模式的六種寫法單例模式
- 單例模式有幾種寫法?單例模式
- 單例模式的五種寫法單例模式
- 單例模式的七種寫法單例模式
- 你真的會寫單例模式嗎?單例模式
- Swift 里正確地 addTarget(_:action:for:)Swift
- 如何快速又正確地在C++裡實現鎖C++
- Java:單例模式的七種寫法Java單例模式
- 單例模式你會幾種寫法?單例模式
- 使用列舉來寫出更優雅的單例設計模式單例設計模式
- 如何建立完美的單例模式?單例模式
- 寫一個 iOS 複雜表單的正確姿勢iOS
- 設計模式(一)單例模式的七種寫法設計模式單例
- 美團一面:會單例模式嗎,寫個單例看看?(8大單例模式實現方式總結)單例模式
- NSACE|如何正確地維護企業的網路資訊保安?
- 如何正確地使用Python的屬性和描述符Python
- 讀《原則》(一):“正確地失敗”
- 確保物件的唯一性——單例模式 (五)物件單例模式
- 確保物件的唯一性——單例模式 (四)物件單例模式
- 確保物件的唯一性——單例模式 (三)物件單例模式
- 確保物件的唯一性——單例模式 (二)物件單例模式
- 如何正確部署 QUICUI
- 面試中單例模式有幾種寫法?面試單例模式
- 單例模式常用的的兩種寫法單例模式