單例模式不是一件小事,快回來看看
上次寫了一篇《單例模式那件小事,看了你不會後悔》的文章,總結了常用的單例模式的實現。本文是上文的延續,單例模式絕不是一件小事,想弄清楚,真不是那麽簡單的。上文提到了常用的三種單例模式的實現方法:餓漢式(除了提前占用資源,沒毛病。),懶漢式(DCL優化過後,沒毛病?),靜態內部類式(優雅的方法,沒毛病。)。文末最後還提到,反射會破壞單例。
本文繼續,雙重檢查鎖定優化過後的懶漢式,真的沒毛病嗎?其實不是,這裏涉及到java編譯器編譯時的一些細節,對象初始化時的寫操作與寫入 sSingleton 字段的操作可以是無序的。這樣的話,如果某個線程調用 getInstance 方法可能看到sSingleton 字段指向了一個 Singleton 對象,但看到該對象裏的字段值卻是默認值,而不是在 Singleton 構造方法裏設置的那些值。這也就是上文提到的,如果不加入 volatile 關鍵字,編譯器可能會失去大量優化的機會或者可能會在編譯時出現一些不可預知的錯誤。那麽加了該關鍵字之後呢,性能會大大降低,有興趣並且由能力的人可以閱讀《Java並發編程實踐》一書,該書將 DCL 懶漢式單例模式形容為“臭名昭著”,不贊成使用。這裏給個延伸鏈接:新的內存模型是否修復了雙重鎖檢查問題?
下面繼續說說我對單例模式的一些理解。
先從上文講到的餓漢式說起:
public class Singleton { /** * 構造方法私有化 */ private Singleton() { } /** * 定義一個私有的靜態的實例 */ private static Singleton sSingleton = new Singleton(); /** * 提供靜態的方法給外界訪問 * * @return */ public static Singleton getInstance() { return sSingleton; } }
下面我將代碼修改為下面的形式:
public class Singleton { public static final Singleton SINGLETON = new Singleton(); private Singleton() { } }
我們不提供對外的 getInstance() 方法獲取實例了,將 SINGLETON 定義為 public,同時將其定義為 final 類型,直接通過 Singleton.SINGLETON 獲取,也沒有問題。私有的構造方法僅會被調用一次,一旦 SINGLETON 被實例化,就只會存在一個實例,外界任何地方都再也不會改變它,我們知道常量就是這麽定義的。當然,跟之前的集中方式一樣,利用反射,還是可以通過私有構造方法創建新對象。除此之外,將對象序列化之後,在反序列化過程中,也會重新創建對象。
如何防止反射破壞單例模式呢?原理上就是在存在一個實例的情況下,再次調用構造方法時,拋出異常。下面以靜態內部類的單例模式為例:
public class Singleton { private static boolean flag = false; private Singleton(){ synchronized(Singleton.class) { if(flag == false) { flag = !flag; } else { throw new RuntimeException("單例模式被侵犯!"); } } } private static class InnerClassSingleton { private final static Singleton sSingleton = new Singleton(); } public static Singleton getInstance() { return InnerClassSingleton.sSingleton; } }
定義了一個 boolean 類型的標誌,判斷是不是第一次調用構造方法,如果不是,即拋出異常。下面測試一下:
public class Test { public static void main(String[] args) { try { Class<Singleton> classType = Singleton.class; Constructor<Singleton> c = classType.getDeclaredConstructor(null); c.setAccessible(true); Singleton s1 = (Singleton)c.newInstance(); Singleton s2 = Singleton.getInstance(); System.out.println(s1==s2); } catch (Exception e) { e.printStackTrace(); } } }
運行結果如下:
Exception in thread "main" java.lang.ExceptionInInitializerError at com.joy.example.Singleton.getInstance(Singleton.java:27) at com.joy.example.Test.main(Singleton.java:17) Caused by: java.lang.RuntimeException: 單例模式被侵犯! at com.joy.example.Singleton.<init>(Singleton.java:16) at com.joy.example.Singleton.<init>(Singleton.java:7) at com.joy.example.Singleton$SingletonHolder.<clinit>(Singleton.java:22) ... 2 more
通過序列化可以講一個對象實例寫入到磁盤中,通過反序列化再讀取回來的時候,即便構造方法是私有的,也依然可以通過特殊的途徑,創建出一個新的實例,相當於調用了該類的構造函數。要避免這個問題,我們需要在代碼中加入如下方法,讓其在反序列化過程中執行 readResolve 方法時返回 sSingleton 對象。
private Object readResolve() throws ObjectStreamException { return sSingleton; }
那有沒有一種方式實現的單例模式在任何情況下都是一個單例呢?有。
枚舉單例
枚舉,就能保證在任何情況下都是單例的,並且是線程安全的。寫法也很簡單:
public enum Singleton{ INSTANCE; // 其它方法 public void doSomething(){ ... } }
雖然枚舉實現單例很簡單,也很安全。但是經驗豐富的 Android 開發人員都會盡量避免使用枚舉。官方文檔有說明:相比於靜態常量Enum會花費兩倍以上的內存。
不管以哪種方式實現單例模式,核心思想都是一樣:將構造方法私有化,然後通過靜態方法獲取唯一的實例對象。這個過程中對線程安全、反序列化操作、對立對象資源消耗、JDK版本等等問題都要考慮到。
單例模式不是一件小事,快回來看看