設計模式(一):單例模式
單例模式是一種常用的軟件設計模式,其定義是單例對象的類只能允許一個實例存在。
單例模式一般體現在類聲明中,單例的類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。
適用場合:
- 需要頻繁的進行創建和銷毀的對象;
- 創建對象時耗時過多或耗費資源過多,但又經常用到的對象;
- 工具類對象;
- 頻繁訪問數據庫或文件的對象。
比如:許多時候整個系統只需要擁有一個的全局對象,這樣有利於我們協調系統整體的行為。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然後服務進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化了在復雜環境下的配置管理。
優點:
- 在內存裏只有一個實例,減少了內存的開銷,尤其是頻繁的創建和銷毀實例(比如網站首頁頁面緩存)。
- 避免對資源的多重占用(比如寫文件操作)。
二、實現方式
1、普通餓漢式(線程安全,不能延時加載)
所謂餓漢。這是個比較形象的比喻。對於一個餓漢來說,他希望他想要用到這個實例的時候就能夠立即拿到,而不需要任何等待時間。
public class Singleton { private final static Singleton INSTANCE = new Singleton(); private Singleton(){} public static Singleton getInstance(){return INSTANCE; } }
優點:寫法簡單 線程安全
通過static
的靜態初始化方式,在該類第一次被加載的時候,就有一個SimpleSingleton
的實例被創建出來了。這樣就保證在第一次想要使用該對象時,他已經被初始化好了。
同時,由於該實例在類被加載的時候就創建出來了,所以也避免了線程安全問題。
JVM類加載機制中:
“ 並發:
虛擬機會保證一個類的類構造器<clinit>()在多線程環境中被正確的加鎖、同步,如果多個線程同時去初始化一個類,那麽只會有一個線程去執行這個類的類構造器<clinit>(),其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。
特別需要註意的是,在這種情形下,其他線程雖然會被阻塞,但如果執行<clinit>()方法的那條線程退出後,其他線程在喚醒之後不會再次進入/執行<clinit>()方法,因為在同一個類加載器下,一個類型只會被初始化一次。 ”
缺點:在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果。
在類被加載的時候對象就會實例化。這也許會造成不必要的消耗,因為有可能這個實例根本就不會被用到。
想象一下,如果實例化instance
很消耗資源,我想讓他延遲加載,另外一方面,我不希望在Singleton
類加載時就實例化,因為我不能確保Singleton
類還可能在其他的地方被主動使用從而被加載,那麽這個時候實例化instance
顯然是不合適的。
解決不能Lazy Loading懶加載問題的辦法:第一種是使用靜態內部類的形式。第二種是使用懶漢式。下文會介紹。
2、靜態代碼塊餓漢式(線程安全,不能延時加載)
public class Singleton { private static Singleton instance; static { instance = new Singleton(); } private Singleton() {} public static Singleton getInstance() { return instance; } }
和第一種一樣,只不過將類實例化的過程放在了靜態代碼塊中,也是在類裝載的時候,就執行靜態代碼塊中的代碼,初始化類的實例。
3、靜態內部類(線程安全,延遲加載,效率高)
public class Singleton { private Singleton() {} private static class SingletonInstance { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonInstance.INSTANCE; } }
加載類 Singleton 時不會實例化對象,加載類 SingletonInstance 時才會實例化對象(也就是調用Singleton的getInstance方法時),實現了延遲加載。
關於類加載機制:JVM類加載機制
優點:線程安全,延遲加載,效率高。
4、枚舉(線程安全,不能延時加載)
public enum Singleton { INSTANCE; public void whateverMethod() { } }
這種方式是Effective Java作者Josh Bloch
提倡的方式,它不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象。
由於1.5中才加入enum
特性,用這種方式寫不免讓人感覺生疏,在實際工作中,我也很少看見有人這麽寫過,但是不代表他不好。
原理其實也是利用類加載機制實現線程安全。
反編譯後:
public final class Singleton extends Enum<Singleton> { public static final Singleton INSTANCE = new Singleton("INSTANCE", 0); private static final Singleton[] $VALUES; public static Singleton[] values() { return (Singleton[])$VALUES.clone(); } public static Singleton valueOf(String string) { return Enum.valueOf(Singleton.class, string); } private Singleton(String string, int n) { super(string, n); } public void whateverMethod() { } static { $VALUES = new Singleton[]{INSTANCE}; } }
關於枚舉原理:JDK源碼學習筆記——Enum枚舉使用及原理
優點:簡單 線程安全
缺點:不能延遲加載 使用較少
5、普通懶漢式(線程不安全,可延時加載)
public class Singleton { private static Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }
優點:可以實現延遲加載
缺點:線程不安全
多個線程可能同時進入if 中,創建出多個實例
6、synchronized 懶漢式(線程安全,可延時加載,效率低)
public class Singleton { private static Singleton singleton; private Singleton() {} public static synchronized Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }
優點:可以實現延遲加載,線程安全
缺點:效率低
只有第一次創建實例的時候需要同步,其他情況都不需要。
我們知道synchronized是一個效率比較低的加鎖方式,而每次獲取實例都會同步加鎖(本身不需要同步,直接返回 instance 即可),效率會很低。
7、雙重校驗鎖懶漢式(線程安全,可延時加載,效率高)
詳細可參考:Java並發(七):雙重檢驗鎖定DCL Java並發(二):Java內存模型
對於第六中方法進行優化,減小鎖的粒度:
public class Singleton { private static Singleton singleton; Integer a; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ // 1 只有singleton==null時才加鎖,性能好 synchronized (Singleton.class){ // 2 if(singleton == null){ // 3 singleton = new Singleton(); // 4 } } } return singleton; } }
會因為重排序出現問題:
線程A發現變量沒有被初始化, 然後它獲取鎖並開始變量的初始化。
由於某些編程語言的語義,編譯器生成的代碼允許在線程A執行完變量的初始化之前,更新變量並將其指向部分初始化的對象。
線程B發現共享變量已經被初始化,並返回變量。由於線程B確信變量已被初始化,它沒有獲取鎖。如果在A完成初始化之前共享變量對B可見(這是由於A沒有完成初始化或者因為一些初始化的值還沒有穿過B使用的內存(緩存一致性)),程序很可能會崩潰。
利用volatile限制重排序:
public class Singleton { private static volatile Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
三、單例與序列化
1、序列化對單例的破壞
雙重檢驗鎖實現單例:
public class Singleton implements Serializable{ private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
測試序列化對單例的影響:
public class SerializableDemo1 { //為了便於理解,忽略關閉流操作及刪除文件操作。真正編碼時千萬不要忘記 //Exception直接拋出 public static void main(String[] args) throws IOException, ClassNotFoundException { //Write Obj to file ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile")); oos.writeObject(Singleton.getSingleton()); //Read Obj from file File file = new File("tempFile"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); Singleton newInstance = (Singleton) ois.readObject(); //判斷是否是同一個對象 System.out.println(newInstance == Singleton.getSingleton()); } } //false
通過對Singleton的序列化與反序列化得到的對象是一個新的對象,這就破壞了Singleton的單例性。
2、分析
ois.readObject(); 調用的 readOrdinaryObject 方法
private Object readOrdinaryObject(boolean unshared) throws IOException { //此處省略部分代碼 Object obj; try { obj = desc.isInstantiable() ? desc.newInstance() : null; } catch (Exception ex) { throw (IOException) new InvalidClassException( desc.forClass().getName(), "unable to create instance").initCause(ex); } //此處省略部分代碼 if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj); if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } if (rep != obj) { handles.setObject(passHandle, obj = rep); } } return obj; }
isInstantiable
:如果一個serializable/externalizable的類可以在運行時被實例化,那麽該方法就返回true。針對serializable和externalizable我會在其他文章中介紹。
desc.newInstance
:該方法通過反射的方式調用無參構造方法新建一個對象。
hasReadResolveMethod:
如果實現了serializable 或者 externalizable接口的類中包含readResolve
則返回true
invokeReadResolve:
通過反射的方式調用要被反序列化的類的readResolve方法。
原因:序列化會通過反射調用無參數的構造方法創建一個新的對象
解決:在Singleton中定義readResolve方法,並在該方法中指定要返回的對象的生成策略,就可以防止單例被破壞。
3、解決
public class Singleton implements Serializable{ private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } privatereturn singleton; } }
總結:一旦實現了Serializable接口之後,就不再是單例的了,因為,每次調用 readObject()方法返回的都是一個新創建出來的對象。解決辦法就是使用readResolve()方法來避免此事發生。
四、關於枚舉實現單例的序列化問題
為了保證枚舉類型像Java規範中所說的那樣,每一個枚舉類型極其定義的枚舉變量在JVM中都是唯一的,在枚舉類型的序列化和反序列化上,Java做了特殊的規定:
在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結果中,反序列化的時候則是通過java.lang.Enum的valueOf方法來根據名字查找枚舉對象。同時,編譯器是不允許任何對這種序列化機制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。
所以,枚舉實現的單例不會有序列化問題。
參考資料 / 相關推薦:
Java並發(二):Java內存模型
Java並發(七):雙重檢驗鎖定DCL
JDK源碼學習筆記——Enum枚舉使用及原理
JVM類加載機制
單例模式的八種寫法比較
設計模式(二)——單例模式
深度分析Java的枚舉類型—-枚舉的線程安全性及序列化問題
設計模式(一):單例模式