深入理解單例模式(上)
最近在閱讀《 》這本書,第3個條款專門提到了單例屬性,並給出了使用單例的最佳實踐建議。讓我對這個單例模式(原本我以為是設計模式中最簡單的一種)有了更深的認識。
單例模式
單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種型別的設計模式屬於建立型模式,它提供了一種建立物件的最佳方式。
在應用這個模式時,單例物件的類必須保證只有一個例項存在。許多時候整個系統只需要擁有一個的全域性物件,這樣有利於我們協調系統整體的行為。
單例的特點
- 單例類只能有一個例項。
- 單例類必須自己建立自己的唯一例項。
- 單例類必須給所有其他物件提供這一例項。
單例模式的7種寫法
單例模式的寫法很多,涉及到了執行緒安全和效能問題。在這裡我不重複介紹。這篇
但是,單例模式真的能夠實現例項的唯一性嗎?答案是否定的。
如何破壞單例
反射
有兩種常見的方式來實現單例。他們的做法都是將構造方法設為私有,並匯出一個公有的靜態成員來提供對唯一實例的訪問。在第1種方式中,成員是個final欄位:
// Singleton with public final field public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } public void leaveTheBuilding() { ... } }
只調用私有建構函式一次,以初始化公共靜態final欄位elvi.instance。不提供公有的或者受保護的建構函式保證了全域性唯一性:當Elvis類初始化的時候,僅僅只會有一個Elvis例項存在——不多也不少 。無論客戶端怎麼做都無法改變這一點,只不過我還是要警告一下 :授權的客戶端可以通過反射來呼叫私有構造方法,藉助於AccessibleObject.setAccessible方法即可做到 。如果需要防範這種攻擊,請修改建構函式,使其在被要求建立第二個例項時丟擲異常。
測試程式碼:
public class TestSingleton { /** * 通過反射破壞單例 */ @Test public void testReflection() throws Exception { /** * 驗證單例有效性 */ Elvis elvis1 = Elvis.INSTANCE; Elvis elvis2 = Elvis.INSTANCE; System.out.println("elvis1 == elvis2 ? ===>" + (elvis1 == elvis2)); System.err.println("-----------------"); /** * 反射呼叫構造方法 */ Class clazz = Elvis.class; Constructor cons = clazz.getDeclaredConstructor(null); cons.setAccessible(true); Elvis elvis3 = (Elvis) cons.newInstance(null); System.out.println("elvis1 == elvis3 ? ===> " + (elvis1 == elvis3)); } }
執行結果:
Elvis Constructor is invoked! elvis1 == elvis2 ? ===> true elvis1 == elvis3 ? ===> false ----------------- Elvis Constructor is invoked!
結論:
反射是可以破壞單例屬性的。因為我們通過反射把它的建構函式設成可訪問的,然後去生成一個新的物件。
改進版的單例寫法:
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() { System.err.println("Elvis Constructor is invoked!"); if (INSTANCE != null) { System.err.println("例項已存在,無法初始化!"); throw new UnsupportedOperationException("例項已存在,無法初始化!"); } } }
結果:
Elvis Constructor is invoked! elvis1 == elvis2 ? ===> true ----------------- Elvis Constructor is invoked! 例項已存在,無法初始化!
第2種實現單例模式的方法是,提供一個公有的靜態工廠方法:
// Singleton with static factory public class Elvis { private static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } public static Elvis getInstance() { return INSTANCE; } public void leaveTheBuilding() { ... } }
所有呼叫Elvis類的getInstance方法,返回相同的物件引用,並且不會有其它的Elvis物件被建立。但同樣有上面第1個方法提到的反射破壞單例屬性的問題存在。
序列化和反序列化
如果對上述2種方式實現的單例類進行序列化,反序列化得到的物件是否是同一個物件呢?答案是否定的。 看下面的測試程式碼: 單例類:
public class Elvis implements Serializable { public static final Elvis INSTANCE = new Elvis(); private Elvis() { System.err.println("Elvis Constructor is invoked!"); } }
測試程式碼:
/** * 序列化對單例屬性的影響 * @throws Exception */ @Test public void testSerialization() throws Exception { Elvis elvis1 = Elvis.INSTANCE; FileOutputStream fos = new FileOutputStream("a.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(elvis1); oos.flush(); oos.close(); Elvis elvis2 = null; FileInputStream fis = new FileInputStream("a.txt"); ObjectInputStream ois = new ObjectInputStream(fis); elvis2 = (Elvis) ois.readObject(); System.out.println("elvis1 == elvis2 ? ===>" + (elvis1 == elvis2)); }
結果是:
Elvis Constructor is invoked! elvis1 == elvis2 ? ===>false
說明:
通過對序列化後的Elvis 進行反序列化得到的物件是一個新的物件,這就破壞了Elvis 的單例性。