單例模式及其反射、反序列化下的漏洞與改進

摘要

  單例模式是軟件項目中最常見的設計模式之一,其主要目的是保證某些對象在項目中的唯一性。
  本文介紹了五種單例模式:餓漢式、懶漢式、雙重檢測式、內部靜態類式、枚舉式,詳細介紹了它們的使用場景,給出了詳細源代碼,並比較了它們的執行效率。最後本文給出了五種單例模式在特殊場景下使用的缺點及其改進方法。

一、餓漢式

  類加載時,就實例化對象,以達到單例的要求,其線程安全,但不能延遲加載。

/**
 * 餓漢式
 * @author 編符俠
 * 2019-07-14
 */
public class SinglePatterns01{
	//類加載時,實例化對象
	private static SinglePatterns01 instance = new SinglePatterns01();
	
	private SinglePatterns01() {
		
	}
	
	public static SinglePatterns01 getInstance() {
		return instance;
	}
}

二、懶漢式

   具有延遲加載的特性,但是由於getInstance()方法爲synchronized方法,所以當多個對象調用該方法時,存在排隊等待,所以效率特別低。

/**
 * 懶漢式
 * 同步方法式
 * @author 編符俠
 * 2019-07-14
 */
public class SinglePatterns02{
	private static SinglePatterns02 instance;
	private SinglePatterns02() {
		
	}
	
	public static synchronized SinglePatterns02 getInstance() {
		if(instance == null) {
			instance = new SinglePatterns02();
		}
		return instance;
	}
}

三、雙重檢測式

  在懶漢式基礎上,將同步方法變爲同步塊,可有效提高效率,同時採用雙重檢測,可過濾掉不需要排隊的對象。

/**
 * 雙重檢測式
 * @author 編符俠
 * 2019-07-14
 */
public class SinglePatterns03{
	private static volatile SinglePatterns03 instance;
	private SinglePatterns03() {
		
	}
	
	public static SinglePatterns03 getInstance() {
		if(instance != null) {
			return instance;
		}
		synchronized(SinglePatterns03.class) {
			if(instance != null) {      // 會有多個線程運行到上一行,再一次判空就會防止多實例化。
				return instance;
			}
			instance = new SinglePatterns03();
		}
		return instance;
	}
}

四、內部靜態類式

  類的創建具有天生的線程安全性,所以將單例封裝在一個內部類中,並在內部類中採用加載時實例化對象的方式,同時採用static final保證其實例唯一性。

/**
 * 靜態內部類實現方式
 * 1、外部類不是static的,所以不會像懶漢式一樣提前創建對象
 * 2、類加載時線程安全的,所以把實例封裝在內部類裏,保證創建時的線程安全性
 * 3、內部類的目標成員變量又用static final修飾,保證了唯一性
 * 
 * 佔用資源多,能延遲加載, 優於懶漢式
 * @author 編符俠
 * 2019-07-14
 */
public class SinglePatterns04{
	private static class SinglePatternsInstance{
		private static final SinglePatterns04 instance = new SinglePatterns04();
	}
	
	private SinglePatterns04() {
		
	}
	
	public static SinglePatterns04 getInstance() {
		return SinglePatternsInstance.instance;
	}
}

五、枚舉式

  枚舉式的單例模式代碼量最少,借鑑其系統機制完成了線程安全的單例模式。其主要缺點是不能延遲加載。

/**
 * 枚舉式的單例模式
 * 佔用資源少,不能延遲加載,優於餓漢式
 * @author 編符俠
 * 2019-07-14
 */
public enum SinglePatterns05 {
	//這就是一個單例
	INSTANCE;
	
	public void handle() {
		//相關的類操作代碼
	}
}

六、五種單例模式的性能對比

  本文采用模擬多線程多對象的情況下,去獲取各個模式下的單例所耗時間。本文采用20個線程,每個線程獲取100萬次對象,對比其耗時結果,代碼及其結果如下:

/**
 * 測試各單例模式的效率
 * @author 編符俠
 * 2019-07-15
 */
public class Client03 {
	
	public static void main(String[] args) throws Exception {		
		long start = System.currentTimeMillis();
		final int THREADNUM = 20;
		// 對線程計數,保證在其他線程結束後,主線程才結束,以此保證統計時間的正確性
		final CountDownLatch count = new CountDownLatch(THREADNUM);
		for(int i = 0; i<THREADNUM;i++){
			new Thread(
					()->{
						for(long j = 0; j<10000000; j++) {
						//  枚舉式的對象獲取
						//	Object instance = SinglePatterns05.INSTANCE;
						//  除枚舉式的對象獲取
							Object instance = SinglePatterns04.getInstance();
						}
						count.countDown();
					}
			).start();
			
		}
		count.await();
		long end = System.currentTimeMillis();
		System.out.println(end-start);
	}
}

  測試結果:

模式 耗時(ms)
餓漢式 254ms
懶漢式 8096ms
雙重檢測式 132ms
靜態內部類式 226ms
枚舉式 136ms

  從上表測試結果可知,懶漢式單例模式效率最低,其原因前面已經提及,它主要是有同步方法,導致其他對象想獲取時,必須排隊等待。

七、反射破解單例模式及其改進

7.1 反射測試單例模式

   採用反射的方式,可以發現上面的設計模式除了枚舉式,其他都不能保證實例爲單例,測試代碼如下:

/**
 * 使用反射方式創建對象的測試
 * 除枚舉式單例模式之外,其他方式的單例模式在使用反射的情況下。
 * 都不能保證單實例,需要改進。
 * 
 * 改進方法:
 * 在構造器中
 * if(null != instance){
 * 	throws new RuntimeException;
 * }
 * @author 編符俠
 * 2019-07-15
 */
public class Client {
	public static void main(String[] args) throws Exception {		
		//使用反射測試各種單例模式的安全性
		//餓漢式
		System.out.println("========= 餓漢式  =========");
		Class<SinglePatterns01> clz01 = (Class<SinglePatterns01>) Class.forName("ft.singlePatterns.SinglePatterns01");
		Constructor<SinglePatterns01> c01 = clz01.getDeclaredConstructor(null);
		c01.setAccessible(true);
		SinglePatterns01 instance11 = (SinglePatterns01) c01.newInstance();
		SinglePatterns01 instance12 = (SinglePatterns01) c01.newInstance();
		System.out.println(instance11);
		System.out.println(instance12);
		
		//懶漢式
		System.out.println("========= 懶漢式  =========");
		Class<SinglePatterns02> clz02 = (Class<SinglePatterns02>) Class.forName("ft.singlePatterns.SinglePatterns02");
		Constructor<SinglePatterns02> c02 = clz02.getDeclaredConstructor(null);
		c02.setAccessible(true);
		SinglePatterns02 instance21 = (SinglePatterns02) c02.newInstance();
		SinglePatterns02 instance22 = (SinglePatterns02) c02.newInstance();
		System.out.println(instance21);
		System.out.println(instance22);
		
		//雙重檢測
		System.out.println("========= 雙重檢測  =========");
		Class<SinglePatterns03> clz03 = (Class<SinglePatterns03>) Class.forName("ft.singlePatterns.SinglePatterns03");
		Constructor<SinglePatterns03> c03 = clz03.getDeclaredConstructor(null);
		c03.setAccessible(true);
		SinglePatterns03 instance31 = (SinglePatterns03) c03.newInstance();
		SinglePatterns03 instance32 = (SinglePatterns03) c03.newInstance();
		System.out.println(instance31);
		System.out.println(instance32);
		
		//靜態內部類式
		System.out.println("========= 靜態內部類式  =========");
		Class<SinglePatterns04> clz04 = (Class<SinglePatterns04>) Class.forName("ft.singlePatterns.SinglePatterns04");
		Constructor<SinglePatterns04> c04 = clz04.getDeclaredConstructor(null);
		c04.setAccessible(true);
		SinglePatterns04 instance41 = (SinglePatterns04) c04.newInstance();
		SinglePatterns04 instance42 = (SinglePatterns04) c04.newInstance();
		System.out.println(instance41);
		System.out.println(instance42);
	}
}

7.2 測試結果

  測試結果如下:

========= 餓漢式  =========
ft.singlePatterns.SinglePatterns01@279f2327
ft.singlePatterns.SinglePatterns01@2ff4acd0
========= 懶漢式  =========
ft.singlePatterns.SinglePatterns02@5caf905d
ft.singlePatterns.SinglePatterns02@27716f4
========= 雙重檢測  =========
ft.singlePatterns.SinglePatterns03@2a84aee7
ft.singlePatterns.SinglePatterns03@a09ee92
========= 靜態內部類式  =========
ft.singlePatterns.SinglePatterns04@452b3a41
ft.singlePatterns.SinglePatterns04@4a574795

7.3 改進方法

  反射是因爲拿到了創建類的圖紙(Class),所以可以創建新的實例,那麼改進方式是在類的構造函數中加入對象是否存在的判斷即可。如下所示:

// 餓漢式單例模式的構造函數
private SinglePatterns01() {
	 if(null != instance){
 	      throws new RuntimeException;
     }
}

八、反序列化破解單例模式及其改進

  採用反序列化測試單例模式時,需要將每個模式類作爲Serializable的實現類,即增加“implement Serializable”。

8.1 反序列化測試單例模式

/**
 * 使用反序列化方式創建對象的測試
 * 除枚舉式單例模式之外,其他方式的單例模式在使用反序列化的情況下。
 * 都不能保證單實例,需要改進。
 * 
 * 改進方法:
 * 在各個單例模式類中重寫readResolve()
 * private Object readResolve() throws ObjectStreamException{
 * 	return instance;
 * }
 * @author 編符俠
 * 2019-07-15
 */
public class Client02 {
	public static void main(String[] args) throws Exception {		
		FileOutputStream fos;
		ObjectOutputStream oos;
		FileInputStream fis;
		ObjectInputStream ois;
		Object instance_g;
		Object instance_s;
		//使用反序列化測試各種單例模式的安全性
		//餓漢式
		System.out.println("========= 餓漢式  =========");
		instance_g = SinglePatterns01.getInstance();
		fos = new FileOutputStream("d:/test01.txt");
		oos = new ObjectOutputStream(fos);
		oos.writeObject(instance_g);
		
		fis = new FileInputStream("d:/test01.txt");
		ois = new ObjectInputStream(fis);
		instance_s = (SinglePatterns01)ois.readObject();
		
		System.out.println(instance_g);
		System.out.println(instance_s);
		
		//懶漢式
		System.out.println("========= 懶漢式  =========");
		instance_g = SinglePatterns02.getInstance();
		fos = new FileOutputStream("d:/test02.txt");
		oos = new ObjectOutputStream(fos);
		oos.writeObject(instance_g);
		
		fis = new FileInputStream("d:/test02.txt");
		ois = new ObjectInputStream(fis);
		instance_s = (SinglePatterns02)ois.readObject();
		
		System.out.println(instance_g);
		System.out.println(instance_s);
		
		//雙重檢測
		System.out.println("========= 雙重檢測  =========");
		instance_g = SinglePatterns03.getInstance();
		fos = new FileOutputStream("d:/test03.txt");
		oos = new ObjectOutputStream(fos);
		oos.writeObject(instance_g);
		
		fis = new FileInputStream("d:/test03.txt");
		ois = new ObjectInputStream(fis);
		instance_s = (SinglePatterns03)ois.readObject();
		
		System.out.println(instance_g);
		System.out.println(instance_s);
		
		//靜態內部類式
		System.out.println("========= 靜態內部類式  =========");
		instance_g = SinglePatterns04.getInstance();
		fos = new FileOutputStream("d:/test04.txt");
		oos = new ObjectOutputStream(fos);
		oos.writeObject(instance_g);
		
		fis = new FileInputStream("d:/test04.txt");
		ois = new ObjectInputStream(fis);
		instance_s = (SinglePatterns04)ois.readObject();
		
		System.out.println(instance_g);
		System.out.println(instance_s);
	}
}

8.2 測試結果

========= 餓漢式  =========
ft.singlePatterns.SinglePatterns01@1d56ce6a
ft.singlePatterns.SinglePatterns01@7a07c5b4
========= 懶漢式  =========
ft.singlePatterns.SinglePatterns02@2ef1e4fa
ft.singlePatterns.SinglePatterns02@306a30c7
========= 雙重檢測  =========
ft.singlePatterns.SinglePatterns03@421faab1
ft.singlePatterns.SinglePatterns03@2b71fc7e
========= 靜態內部類式  =========
ft.singlePatterns.SinglePatterns04@69d0a921
ft.singlePatterns.SinglePatterns04@446cdf90

8.3 改進方法

   反序列化創建對象時,會調用Serializable類的成員函數readResolve(),所以各個實例模式需要重寫readResolve()方法,如下所示:

private Object readResolve() throws ObjectStreamException{
  	return instance;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章