單例模式——顧名思義即在既定的業務場景下某一實體類只需存在一個對象,就能充分的處理所有的業務需求。而且在某種現場環境下,創建這樣的對象對系統性能的開銷非常大。正因為這種特性,單利模式通常具有節省系統開銷的效果。我將從以下幾個方面對一些常見的單利模式進行總結歸納,在下才疏學淺,不曾賣弄,旨在知識重溫與記錄。有所疏忽,請各位不吝指正,自當感激不盡。
歸納層面:
常見的單利模式以及實現方式。
產品級單例模式的穿透方式以及防范方法。
常見的單利模式的並發性能測試。
一,常見的單利模式以及實現方式
在實現層面上,目前的幾種主要的單例模式往往有以下幾項性能指標作為選型參考:
-- 是否實現延遲加載
-- 是否線程安全
-- 並發訪問性能
-- 是否可以防止反射與反序列化穿透
經過一段時間的工作和學習,將自己所遇到的幾種單例模式作如下比較總結,當然,也作為自己學習復習的一種方式。
<1>,餓漢式單例模式。
/** * 未實現延遲加載 * 線程安全 * @author xinz * */ public class Singleton1 { private Singleton1(){} private static Singleton1 instance = new Singleton1(); public static Singleton1 getInstance(){ return instance; } }
<2>,懶漢式單例模式
/** * 實現延遲加載 * 線程安全但犧牲高並發性能 * @author xinz */ public class Singleton2 { private Singleton2(){ } private static Singleton2 instance; public static synchronized Singleton2 getInstance(){ if(instance == null){ instance = new Singleton2(); } return instance; } }
<3>,雙重檢測鎖式單例模式
/** * 雙重檢測鎖式單例模式 * 實現了延遲加載 * 線程安全 * @author xinz * */ public class Singleton3 { private static Singleton3 instance = null; private Singleton3() {} public static Singleton3 getInstance() { if (instance == null) { Singleton3 sc; synchronized (Singleton3.class) { sc = instance; if (sc == null) { synchronized (Singleton3.class) { if (sc == null) { sc = new Singleton3(); } } instance = sc; } } } return instance; } }
<4>,靜態內部類式單例模式
/** * 靜態內部類單利模式 * 線程安全 * 實現延遲加載 * @author xinz * */ public class Singleton4 { private Singleton4 (){} /** * 外部類初始化的時候不會初始化該內部類 * 只有當調用getInstance方法時候才會初始化 */ public static class inner{ public static final Singleton4 instance = new Singleton4(); } public static Singleton4 getInstance(){ return inner.instance; } }
<5>,枚舉式單例模式
/** * 未延遲加載 * 線程安全 * 原生防止反射與反序列話擊穿 * @author xinz */ public enum Singleton5 { INSTANCE; public static Object doSomething(){ //添加其他功能邏輯。。。。。。 return null; } }
對於以上5種單例模式作如下簡單測試:
/** * 測試單利是否返回相同對象 * @author xinz * */ public class TestSingleton { public static void main(String[] args) { /** * 餓漢式 */ Singleton1 singleton1_1 = Singleton1.getInstance(); Singleton1 singleton1_2 = Singleton1.getInstance(); System.out.println(singleton1_1 == singleton1_1);//true /** * 懶漢式 */ Singleton2 singleton2_1 = Singleton2.getInstance(); Singleton2 singleton2_2 = Singleton2.getInstance(); System.out.println(singleton2_1 == singleton2_1);//true /** * 雙重檢測鎖式 */ Singleton3 singleton3_1 = Singleton3.getInstance(); Singleton3 singleton3_2 = Singleton3.getInstance(); System.out.println(singleton3_1 == singleton3_1);//true /** * 靜態內部類式 */ Singleton4 singleton4_1 = Singleton4.getInstance(); Singleton4 singleton4_2 = Singleton4.getInstance(); System.out.println(singleton4_1 == singleton4_1);//true /** * 枚舉式 */ Singleton5 singleton5_1 = Singleton5.INSTANCE; Singleton5 singleton5_2 = Singleton5.INSTANCE; /* * 枚舉型的任何成員類型都是類實例的類型 */ System.out.println(singleton5_1.getClass());//class com.xinz.source.Singleton5 System.out.println(singleton5_1 == singleton5_1);//true } }
綜上,5種實現單例模式的方法,都能基本實現現有系統目標對象的唯一性。區別在於是否能夠延遲加載進一步節約系統性能。其中“雙重檢測鎖式”由於JVM底層在執行同步塊的嵌套時有時會發生漏洞,所以在JDK修復該漏洞之前,該方式不建議使用。
二,產品級單例模式的穿透方式以及防范方法
關於以上的五種單利模式的實現方式,對一般的Web應用開發,我們無需考慮誰會來試圖破解我們的單利限制。但如果開發是面向產品級,那么我們將不得不考慮單例破解問題,常見的單例模式多見於反射穿透與序列化破解。
<1>,防止反射穿透。
對於反射,我們知道只要有構造方法,不做處理的情況下,即使私有化構造器,也沒辦阻止反射調用得到對象。從而使既有系統存在多個對象。如下,我們使用餓漢式單例模式為例,進行反射穿透。代碼如下:
/* * 反射破解餓漢式單例模式 */ public class TestReflectSingleton { public static void main(String[] args) throws Exception { Class<Singleton1> clazz = (Class<Singleton1>) Class.forName("com.xinz.source.Singleton1"); Constructor<Singleton1> constructor = clazz.getDeclaredConstructor(null); //強制設置構造器可訪問 constructor.setAccessible(true); Singleton1 s1 = constructor.newInstance(); Singleton1 s2 = constructor.newInstance(); System.out.println(s1==s2);//false } }
那么很顯然,像餓漢式,懶漢式,雙重檢測鎖式,靜態內部類事,這幾種只要有構造器的單例模式就會存在被反射穿透的風險。而第五種枚舉式單例模式,原生不存在構造器,所以避免了反射穿透的風險。
對於前邊四種存在反射穿透的單例模式,我們的解決思路就是,萬一有人通過反射進入到構造方法,那么我們可以考慮拋異常,代碼如下:
/** * 實現延遲加載 * 線程安全但犧牲高並發性能 * @author xinz */ public class Singleton2 { /* * 如果有反射進入構造器,判斷后拋異常,這樣的話一旦初始化 instance 對象 * 反射調用便會被阻止,初始化之前還是可以被反射的 */ private Singleton2(){ if(instance != null){ throw new RuntimeException(); } } private static Singleton2 instance; public static synchronized Singleton2 getInstance(){ if(instance == null){ instance = new Singleton2(); } return instance; } }
測試代碼:
import java.lang.reflect.Constructor; public class TestSingleton { public static void main(String[] args) throws Throwable { Singleton2 s1 = Singleton2.getInstance(); Singleton2 s2 = Singleton2.getInstance(); System.out.println(s1); System.out.println(s1); Class<Singleton2> clazz = (Class<Singleton2>) Class.forName("com.xinz.source.Singleton2"); Constructor<Singleton2> c = clazz.getDeclaredConstructor(null); c.setAccessible(true); Singleton2 s3 = c.newInstance(); Singleton2 s4 = c.newInstance(); System.out.println(s3); System.out.println(s4); } }
執行結果:
com.xinz.source.Singleton2@2542880d com.xinz.source.Singleton2@2542880d Exception in thread "main" java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:526) at com.xinz.source.TestSingleton.main(TestSingleton.java:17) Caused by: java.lang.RuntimeException at com.xinz.source.Singleton2.<init>(Singleton2.java:15) ... 5 more
即一旦初始化完成后,反射就會報錯。但無法阻止反射發生在初始化之前,代碼如下:
import java.lang.reflect.Constructor; public class TestSingleton { public static void main(String[] args) throws Throwable { Class<Singleton2> clazz = (Class<Singleton2>) Class.forName("com.xinz.source.Singleton2"); Constructor<Singleton2> c = clazz.getDeclaredConstructor(null); c.setAccessible(true); Singleton2 s3 = c.newInstance(); Singleton2 s4 = c.newInstance(); System.out.println(s3); System.out.println(s4); Singleton2 s1 = Singleton2.getInstance(); Singleton2 s2 = Singleton2.getInstance(); System.out.println(s1); System.out.println(s1); } }
測試結果如下:
com.xinz.source.Singleton2@32f22097
com.xinz.source.Singleton2@3639b3a2
com.xinz.source.Singleton2@6406c7e
com.xinz.source.Singleton2@6406c7e
很顯然反射得到的兩個對象不是同一對象。目前尚未找到解決策略,還望高手指點。
<2>,反序列化破解
反序列化即先將系統里邊唯一的單實例對象序列化到硬盤,然后在反序列化,得到的對象默認和原始對象屬性一致,但已經不是同一對象了。如下:
/** * 反序列化創建新對象 * @author xizn */ public class TestSingleton { public static void main(String[] args) throws Throwable { Singleton2 s1 = Singleton2.getInstance(); System.out.println(s1); //通過反序列化的方式構造多個對象 FileOutputStream fos = new FileOutputStream("d:/a.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s1); oos.close(); fos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/a.txt")); Singleton2 s3 = (Singleton2) ois.readObject(); System.out.println(s3); } }
測試結果如下:(當然,目標類要實現序列化接口)
com.xinz.source.Singleton2@3639b3a2
com.xinz.source.Singleton2@46e5590e
如何防止這種破解單利模式,我們采取重寫反序列化方法 -- readResolve() 最終防止單利被破解的代碼如下(這里僅以懶漢式為例,其它類似):
import java.io.ObjectStreamException; import java.io.Serializable; /** * 實現延遲加載 * 線程安全但犧牲高並發性能 * @author xinz */ public class Singleton2 implements Serializable { /* * 如果有反射進入構造器,判斷后拋異常,這樣的話一旦初始化 instance 對象 * 反射調用便會被阻止,初始化之前還是可以被反射的 */ private Singleton2(){ if(instance != null){ throw new RuntimeException(); } } private static Singleton2 instance; public static synchronized Singleton2 getInstance(){ if(instance == null){ instance = new Singleton2(); } return instance; } //反序列化時,如果定義了readResolve()則直接返回此方法指定的對象。而不需要單獨再創建新對象! private Object readResolve() throws ObjectStreamException { return instance; } }
還是上邊的測試代碼,測試結果:
com.xinz.source.Singleton2@6f92c766
com.xinz.source.Singleton2@6f92c766
三,常見的單利模式的並發性能測試
測試我們啟用20個線程,每個線程循環獲取單例對象100萬次,測試代碼:
/** * 並發性能測試 * @author xizn */ public class TestSingleton { public static void main(String[] args) throws Throwable { long start = System.currentTimeMillis(); int threadNum = 20; final CountDownLatch countDownLatch = new CountDownLatch(threadNum); for(int i=0;i<threadNum;i++){ new Thread(new Runnable() { @Override public void run() { for(int i=0;i<1000000;i++){ // Object o1 = Singleton1.getInstance(); // Object o2 = Singleton2.getInstance(); // Object o3 = Singleton3.getInstance(); // Object o4 = Singleton4.getInstance(); Object o5 = Singleton5.INSTANCE; } countDownLatch.countDown(); } }).start(); } countDownLatch.await(); //main線程阻塞,直到計數器變為0,才會繼續往下執行! long end = System.currentTimeMillis(); System.out.println("總耗時:"+(end-start)); } }
執行結果根據電腦性能每個人可能會有不同的結果,但大概還是可以反映出性能優劣:
餓漢式 | 總耗時:10毫秒 | 不支持延遲加載,一般不能防范反射與反序列化 |
懶漢式 | 總耗時:498毫秒 | 支持延遲加載,一般不能防范反射與反序列化,並發性能差 |
雙重檢測鎖式 | 總耗時:11毫秒 | JVM底層支持不太好,其它性能同餓漢式 |
靜態內部類式 | 總耗時:12毫秒 | 一般不能防范反射與反序列化,其它性能良好 |
枚舉式 | 總耗時:12毫秒 | 未實現延遲加載,原生防范反射與反序列化,其它性能良好 |
綜上測試結果,個人認為:
對於要求延遲加載的系統,靜態內部類式優於懶漢式。
對於產品級別,要求安全級別高的系統,枚舉式優於餓漢式。
雙重檢測鎖式單例模式在JDK修復同步塊嵌套漏洞之前不推薦。
寫了大半天,總算對自己的學習內容總結告一段落,在此,特別感謝高淇、白鶴翔兩位老師。