單例模式多是代碼最少的模式了,可是少不必定意味着簡單,想要用好、用對單例模式,還真得費一番腦筋。本文對Java中常見的單例模式寫法作了一個總結,若有錯漏之處,懇請讀者指正。 html
顧名思義,餓漢法就是在第一次引用該類的時候就建立對象實例,而無論實際是否須要建立。代碼以下: java
public class Singleton { private static Singleton = new Singleton(); private Singleton() {} public static getSignleton(){ return singleton; } }
這樣作的好處是編寫簡單,可是沒法作到延遲建立對象。可是咱們不少時候都但願對象能夠儘量地延遲加載,從而減少負載,因此就須要下面的懶漢法: android
這種寫法是最簡單的,由私有構造器和一個公有靜態工廠方法構成,在工廠方法中對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; } }
可是,上面提到的全部實現方式都有兩個共同的缺點:
固然,還有一種更加優雅的方法來實現單例模式,那就是枚舉寫法:
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高級特性與最佳實踐(第二版)》