單例模式是設計模式中使用比較廣泛的模式之一,它是一種對象建立模式,用於產生一個對象的實例,並能確保系統中一個類只產生一個實例,這樣能夠帶來兩大好處:java
(1)對於頻繁使用的對象實例,能夠省略建立對象實例所花費的時間,這對於那些重量級的對象而言,是一筆很是可觀的系統開銷。設計模式
(2)因爲new操做的次數減小,能夠減輕GC壓力,縮短GC停頓時間。安全
綜上可知,對於系統關鍵組件和被頻繁使用的對象實例,使用單例模式能夠有效的改善系統的性能。性能優化
下面分析單例模式的幾種實現方式:多線程
先看代碼以下:併發
package com.jason.design.singleton; /** * 簡單的單例模式實現,可是不是懶加載形式 */ public class SingletonClass1 { private static final SingletonClass1 instance = new SingletonClass1(); private SingletonClass1(){} public static SingletonClass1 getInstance(){ return instance; } }
首先,類的構造函數寫成private的,從而保證別的類不能實例化此類。其次,在類中提供一個靜態的實例instance並提供靜態的getInstance方法給使用者。這樣,使用者就能夠經過這個引用使用到這個類的實例了。函數
上述單例的實現方式很簡單,而且很是可靠。可是有一個問題:不管這個類是否被使用,都會建立一個instance對象。若是這個建立過程很耗時,而且這個類還並不必定會被使用,那麼這個建立過程就是無用的。咱們作延遲加載優化:性能
package com.jason.design.singleton; /** * 延遲加載可是非線程安全的單例模式 */ public class SingletonClass2 { private static SingletonClass2 instance = null; private SingletonClass2(){} public static SingletonClass2 getInstance(){ if (instance == null){ instance = new SingletonClass2(); } return instance; } }
首先,把instance初始化爲null,確保系統啓動時沒有額外的負載,由於建立過程不在聲明處,因此那個final的修飾必須去掉。優化
其次,在getInstance()方法中,判斷當前單例是否已經存在,若存在則返回,不存在再創建單例。spa
上述代碼在單線程狀況下,沒有什麼問題,但是若是是多線程,會有線程安全問題,在併發狀況下,線程可能各自擁有一個SingletonClass2的對象。
繼續對其進行優化,首先想到的是加鎖,在getInstance()加上同步鎖,一個線程必須等待另一個線程建立完成後才能使用這個方法,這就保證了單例的惟一性。
package com.jason.design.singleton; /** * 同步獲取單例方法的單例模式,這樣會致使每次獲取單例時都是同步方法,粒度過大 */ public class SingletonClass3 { private static SingletonClass3 instance = null; private SingletonClass3(){} public static synchronized SingletonClass3 getInstance(){ if (instance == null){ instance = new SingletonClass3(); } return instance; } }
上述代碼引入了synchronized鎖,並且是加在整個方法上的,這樣性能可能受到影響。分析一下不難發現出現3中線程安全問題緣由是檢測null的操做和建立對象的操做分離了。若是這兩個操做可以原子地進行就能保證線程安全。因此,能夠將同步塊進行縮小粒度,首先去掉getInstance()的同步操做,而後把同步鎖加載if語句上,修改以下:
package com.jason.design.singleton; public class SingletonClass4 { private static SingletonClass4 instance = null; private SingletonClass4() { } public static SingletonClass4 getInstance() { synchronized (SingletonClass4.class) { if (instance == null) { instance = new SingletonClass4(); } } return instance; } }
繼續分析發現,即便縮小了同步塊的粒度,每次調用getInstance()的時候仍是須要每次都調用同步塊中的內容,性能問題仍是存在。此時可用到Double-check的模式,繼續修改以下:
package com.jason.design.singleton; /** * Double-Check模式的單例模式實現 */ public class SingletonClass5 { private static SingletonClass5 instance = null; private SingletonClass5(){} private static SingletonClass5 getInstance(){ if (instance == null) { synchronized (SingletonClass5.class) { if (instance == null) { instance = new SingletonClass5(); } } } return instance; } }
首先判斷instance是否是爲null,若是爲null,加鎖初始化;若是不爲null,直接返回instance。可是這樣就夠了嗎?恰好最近在閱讀Dubbo源碼的時候看到了這種double-check的應用:在Dubbo源碼com/jason/properties/ConfigUtils.java類的時候有這樣一段代碼:
private static volatile Properties PROPERTIES; public static Properties getProperties() { if (PROPERTIES == null) { synchronized (ConfigUtils.class) { if (PROPERTIES == null) { String path = System.getProperty(Constants.DUBBO_PROPERTIES_KEY); if (path == null || path.length() == 0) { path = System.getenv(Constants.DUBBO_PROPERTIES_KEY); if (path == null || path.length() == 0) { path = Constants.DEFAULT_DUBBO_PROPERTIES; } } PROPERTIES = ConfigUtils.loadProperties(path, false, true); } } } return PROPERTIES; }
對比代碼能夠發現,dubbo中的實現對於PROPERTIES的修飾符多了一個volatile,這個區別在哪裏?
下面來想一下,建立一個變量須要哪些步驟呢?一個是申請一塊內存,調用構造方法進行初始化操做,另外一個是分配一個指針指向這塊內存。這兩個操做誰在前誰在後呢?JVM規範並無規定。那麼就存在這麼一種狀況,JVM是先開闢出一塊內存,而後把指針指向這塊內存,最後調用構造方法進行初始化。
下面咱們來考慮這麼一種狀況:線程A開始建立SingletonClass的實例,此時線程B調用了getInstance()方法,首先判斷instance是否爲null。按照咱們上面所說的內存模型,A已經把instance指向了那塊內存,只是尚未調用構造方法,所以B檢測到instance不爲null,因而直接把instance返回了——問題出現了,儘管instance不爲null,但它並無構造完成,就像一套房子已經給了你鑰匙,但你並不能住進去,由於裏面尚未收拾。此時,若是B在A將instance構造完成以前就是用了這個實例,程序就會出現錯誤了!
在JDK 5以後,Java使用了新的內存模型。volatile關鍵字有了明確的語義——在JDK1.5以前,volatile是個關鍵字,可是並無明確的規定其用途——被volatile修飾的寫變量不能和以前的讀寫代碼調整,讀變量不能和以後的讀寫代碼調整!所以,只要咱們簡單的把instance加上volatile關鍵字就能夠了。
package com.jason.design.singleton; /** * Double-Check模式的單例模式實現,而且加入了volatile的,可保證安全 */ public class SingletonClass6 { private static volatile SingletonClass6 instance = null; private SingletonClass6(){} private static SingletonClass6 getInstance(){ if (instance == null) { synchronized (SingletonClass6.class) { if (instance == null) { instance = new SingletonClass6(); } } } return instance; } }
然而,這只是JDK1.5以後的Java的解決方案,那以前版本呢?其實,還有另外的一種解決方案,並不會受到Java版本的影響,即:Initialization on Demand Holder(IODH),詳見 http://en.wikipedia.org/wiki/Initialization_on_demand_holder_idiom
package com.jason.design.singleton; /** * 靜態內部類實現的單例模式 */ public class SingletonClass7 { private SingletonClass7(){} private static class SingletonInstance{ private static final SingletonClass7 instance = new SingletonClass7(); } public SingletonClass7 getInstance(){ return SingletonInstance.instance; } }
由於SingletonClass7沒有static的屬性,所以在沒有被調用時並不會被初始化,知足延遲加載的優勢,直到調用getInstance()的時候,會首先加載SingletonClassInstance類,這個類有一個static的SingletonClass實例,所以須要調用SingletonClass的構造方法,而後getInstance()將把這個內部類的instance返回給使用者。因爲這個instance是static的,所以並不會構造屢次。
因爲SingletonClassInstance是私有靜態內部類,因此不會被其餘類知道,一樣,static語義也要求不會有多個實例存在。而且,JSL規範定義,類的構造必須是原子性的,非併發的,所以不須要加同步塊。一樣,因爲這個構造是併發的,因此getInstance()也並不須要加同步。
綜上:使用內部類的方式實現單例,便可以作到延遲加載,也沒必要使用同步,是一種比較好的實現方式。
代碼以下:
package com.jason.design.singleton; /** * 枚舉的方式實現單例模式 */ public enum EnumSingleton { INSTANCE; private SingletonClass8 instance = null; private EnumSingleton() { instance = new SingletonClass8(); } public SingletonClass8 getInstance(){ return instance; } }
單例使用方式:
EnumSingleton.INSTANCE.getInstance();
下面咱們來看看單例是如何被保證的:
首先,在枚舉中咱們明確了構造方法限制爲私有,在咱們訪問枚舉實例時會執行構造方法,同時每一個枚舉實例都是static final類型的,也就代表只能被實例化一次。在調用構造方法時,咱們的單例被實例化。
也就是說,由於enum中的實例被保證只會被實例化一次,因此咱們的INSTANCE也被保證明例化一次。
能夠看到,枚舉實現單例仍是比較簡單的,除此以外咱們再來看一下Enum這個類的聲明:
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable
/** * prevent default deserialization */ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { throw new InvalidObjectException("can't deserialize enum"); }
/** * Throws CloneNotSupportedException. This guarantees that enums * are never cloned, which is necessary to preserve their "singleton" * status. * * @return (never returns) */ protected final Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); }
綜上,單元素枚舉實現單例模式有以下優勢:(1)、自由序列化 (2)、保證只有一個實例(即便使用反射機制也沒法屢次實例化一個枚舉量)(3)、JVM保證的線程安全
對象的建立方式有以下四中方式:(1)new (2)克隆(3)序列化(4)反射 在上面實現的方式中,因爲構造函數是private的,所以第一種方式沒法破壞目前咱們實現的單例模式。那麼是否還有能夠經過其餘的三種方式建立對象呢,但其餘三種方式會不會破壞單例模式:
由克隆咱們能夠想到原型模式,原型模式就是經過clone方法實現對象的建立的,clone方式是Object方法,每一個對象都有,那我使用一個單例模式類的對象,調用clone方法,再建立一個新的對象了,那豈不是上面說的單例模式失效了。固然答案是否認,某一個對象直接調用clone方法,會拋出異常,即並不能成功克隆一個對象。調用該方法時,必須實現一個Cloneable 接口。這也就是原型模式的實現方式。還有即若是該類實現了cloneable接口,儘管構造函數是私有的,他也能夠建立一個對象。即clone方法是不會調用構造函數的,他是直接從內存中copy內存區域的。因此單例模式的類是不能夠實現cloneable接口的。
序列化一是能夠實現數據的持久化;二是能夠對象數據的遠程傳輸。 若是過該類implements Serializable,那麼就會在反序列化的過程當中再創一個對象。這個問題的解決辦法就是在反序列化時,指定反序化的對象實例,代碼以下:
package com.jason.design.singleton; import java.io.Serializable; /** * Double-Check模式的單例模式實現,而且加入了volatile的,可保證安全,可被串行化的單例 */ public class SingletonClass8 implements Serializable { private static volatile SingletonClass8 instance = null; private SingletonClass8(){} private static SingletonClass8 getInstance(){ if (instance == null) { synchronized (SingletonClass8.class) { if (instance == null) { instance = new SingletonClass8(); } } } return instance; } /** * 在反序列化時,指定反序化的對象實例 * @return */ private Object readResolve(){ return instance; } }
反射是能夠獲取類的構造函數,再加一行 setAccessible(true);就能夠調用私有的構造函數,建立對象了。那麼防止反射破壞Java單例模式的方法就是:當第二次調用構造函數時拋出異常。代碼以下:
package com.jason.design.singleton; /** * 靜態內部類實現的單例模式,防止反射機制破壞單例 */ public class SingletonClass10 { private static volatile boolean flag = true; private SingletonClass10(){ if (flag){ flag = false; }else { throw new RuntimeException(); } } private static class SingletonInstance{ private static final SingletonClass10 instance = new SingletonClass10(); } public SingletonClass10 getInstance(){ return SingletonInstance.instance; } }
參考文獻:http://www.jb51.net/article/80201.htm
http://blog.51cto.com/devbean/203501
http://blog.csdn.net/yy254117440/article/details/52305175
http://blog.csdn.net/chao_19/article/details/51112962