標籤:
你真的會寫單例模式嗎——Java實現
原文:http://www.tuicool.com/articles/MBrUfy6
單例模式可能是代碼最少的模式了,但是少不一定意味著簡單,想要用好、用對單例模式,還真得費一番腦筋。本文對Java中常見的單例模式寫法做了一個總結,如有錯漏之處,懇請讀者指正。
餓漢法
顧名思義,餓漢法就是在第一次引用該類的時候就建立對象執行個體,而不管實際是否需要建立。代碼如下:
public class Singleton { private static Singleton = new Singleton(); private Singleton() {} public static getSignleton(){ return singleton; }}
這樣做的好處是編寫簡單,但是無法做到延遲建立對象。但是我們很多時候都希望對象可以儘可能地消極式載入,從而減小負載,所以就需要下面的懶漢法:
單線程寫法
這種寫法是最簡單的,由私人構造器和一個公有靜態Factory 方法構成,在Factory 方法中對singleton進行null判斷,如果是null就new一個出來,最後返回singleton對象。這種方法可以實現延時載入,但是有一個致命弱點:線程不安全。如果有兩條線程同時調用getSingleton()方法,就有很大可能導致重複建立對象。
public class Singleton { private static Singleton singleton = null; private Singleton(){} public static Singleton getSingleton() { if(singleton == null) singleton = new Singleton(); return singleton; }}
考慮安全執行緒的寫法
這種寫法考慮了安全執行緒,將對singleton的null判斷以及new的部分使用synchronized進行加鎖。同時,對singleton對象使用volatile關鍵字進行限制,保證其對所有線程的可見度,並且禁止對其進行指令重排序最佳化。如此即可從語義上保證這種單例模式寫法是安全執行緒的。注意,這裡說的是語義上,實際使用中還是存在小坑的,會在後文寫到。
public class Singleton { private static volatile Singleton singleton = null; private Singleton(){} public static Singleton getSingleton(){ synchronized (Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } return singleton; } }
兼顧安全執行緒和效率的寫法
雖然上面這種寫法是可以正確啟動並執行,但是其效率低下,還是無法實際應用。因為每次調用getSingleton()方法,都必須在synchronized這裡進行排隊,而真正遇到需要new的情況是非常少的。所以,就誕生了第三種寫法:
public class Singleton { private static volatile Singleton singleton = null; private Singleton(){} public static Singleton getSingleton(){ if(singleton == null){ synchronized (Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } } return singleton; } }
這種寫法被稱為“雙重檢查鎖”,顧名思義,就是在getSingleton()方法中,進行兩次null檢查。看似多此一舉,但實際上卻極大提升了並發度,進而提升了效能。為什麼可以提高並發度呢?就像上文說的,在單例中new的情況非常少,絕大多數都是可以並行的讀操作。因此在加鎖前多進行一次null檢查就可以減少絕大多數的加鎖操作,執行效率提高的目的也就達到了。
坑
那麼,這種寫法是不是絕對安全呢?前面說了,從語義角度來看,並沒有什麼問題。但是其實還是有坑。說這個坑之前我們要先來看看volatile這個關鍵字。其實這個關鍵字有兩層語義。第一層語義相信大家都比較熟悉,就是可見度。可見度指的是在一個線程中對該變數的修改會馬上由工作記憶體(Work Memory)寫回主記憶體(Main Memory),所以會馬上反應在其它線程的讀取操作中。順便一提,工作記憶體和主記憶體可以近似理解為實際電腦中的快取和主存,工作記憶體是線程獨享的,主存是線程共用的。volatile的第二層語義是禁止指令重排序最佳化。大家知道我們寫的代碼(尤其是多線程代碼),由於編譯器最佳化,在實際執行的時候可能與我們編寫的順序不同。編譯器只保證程式執行結果與原始碼相同,卻不保證實際指令的順序與原始碼相同。這在單線程看起來沒什麼問題,然而一旦引入多線程,這種亂序就可能導致嚴重問題。volatile關鍵字就可以從語義上解決這個問題。
注意,前面反覆提到“從語義上講是沒有問題的”,但是很不幸,禁止指令重排最佳化這條語義直到jdk1.5以後才能正確工作。此前的JDK中即使將變數聲明為volatile也無法完全避免重排序所導致的問題。所以,在jdk1.5版本前,雙重檢查鎖形式的單例模式是無法保證安全執行緒的。
靜態內部類法
那麼,有沒有一種延時載入,並且能保證安全執行緒的簡單寫法呢?我們可以把Singleton執行個體放到一個靜態內部類中,這樣就避免了靜態執行個體在Singleton類載入的時候就建立對象,並且由於靜態內部類只會被載入一次,所以這種寫法也是安全執行緒的:
public class Singleton { private static class Holder { private static Singleton singleton = new Singleton(); } private Singleton(){} public static Singleton getSingleton(){ return Holder.singleton; }}
但是,上面提到的所有實現方式都有兩個共同的缺點:
- 都需要額外的工作(Serializable、transient、readResolve())來實現序列化,否則每次還原序列化一個序列化的對象執行個體時都會建立一個新的執行個體。
- 可能會有人使用反射強行調用我們的私人構造器(如果要避免這種情況,可以修改構造器,讓它在建立第二個執行個體的時候拋異常)。
枚舉寫法
當然,還有一種更加優雅的方法來實現單例模式,那就是枚舉寫法:
public enum Singleton { INSTANCE; private String name; public String getName(){ return name; } public void setName(String name){ this.name = name; }}
使用枚舉除了安全執行緒和防止反射強行調用構造器之外,還提供了自動序列化機制,防止還原序列化的時候建立新的對象。因此, Effective Java 推薦儘可能地使用枚舉來實現單例。
總結
這篇文章發出去以後得到許多反饋,這讓我受寵若驚,覺得應該再寫一點小結。代碼沒有一勞永逸的寫法,只有在特定條件下最合適的寫法。在不同的平台、不同的開發環境(尤其是jdk版本)下,自然有不同的最優解(或者說較優解)。
比如枚舉,雖然Effective Java中推薦使用,但是在Android平台上卻是不被推薦的。在 這篇Android Training 中明確指出:
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
再比如雙重檢查鎖法,不能在jdk1.5之前使用,而在Android平台上使用就比較放心了(一般Android都是jdk1.6以上了,不僅修正了volatile的語義問題,還加入了不少鎖最佳化,使得多線程同步的開銷降低不少)。
最後,不管採取何種方案,請時刻牢記單例的三大要點:
參考資料
《Effective Java(第二版)》
《深入理解Java虛擬機器——JVM進階特性與最佳實務(第二版)》
[轉] 你真的會寫單例模式嗎——Java實現