Java學習筆記之--------單例模式(二)

單例模式存在的問題

1.反射可以破解單例模式。

2.反序列化可以破解單例模式。

注:上面的單例模式不包括枚舉實現單例模式。

反射破解單例模式

我們以懶漢式爲例。

public class SingletonDemo06 {

	private static SingletonDemo06 instance;

	private SingletonDemo06(){
		/*if (instance != null){
			throw new RuntimeException();
		}*/
	}

	public static synchronized SingletonDemo06 getInstance(){
		if (instance == null){
			instance = new SingletonDemo06();
		}
		return instance;
	}

}
public class Client2 {

	public static void main(String[] args) throws Exception {

		SingletonDemo06 s1 = SingletonDemo06.getInstance();
		SingletonDemo06 s2 = SingletonDemo06.getInstance();

		System.out.println(s1);
		System.out.println(s2);

		Class<SingletonDemo06> clazz =
				(Class<SingletonDemo06>) Class.forName("com.sxt.singleton.SingletonDemo06");

		Constructor<SingletonDemo06> c = clazz.getDeclaredConstructor(null);
		//c.setAccessible(true);
		SingletonDemo06 s3 = c.newInstance();
		SingletonDemo06 s4 = c.newInstance();

		System.out.println(s3);
		System.out.println(s4);

	}

}

運行上面的Client2,我們可以看到以下報錯信息:

Client2無法訪問SingletonDemo06的私有成員。

我們將Client2中的 c.setAccessible(true); 這行代碼的註釋解開,再次運行Client2,可以看到以下運行結果:

可以看到上s3和s4不是同一個對象。我們跳過了單例模式,new了兩個對象。

如何防止反射破解單例模式呢?通過在構造器中拋出異常的方法來實現,將SingletonDemo06中的註釋解開,然後再運行Client2,將會拋出異常,可以看到,s3和s4沒有創建成功:

結論:可以在構造方法中手動拋出異常,來避免反射破解單例模式。

反序列化可以破解單例模式

SingletonDemo06需要實現序列化的接口。

public class SingletonDemo06 implements Serializable {

	private static SingletonDemo06 instance;

	//私有化構造器
	private SingletonDemo06(){
		if (instance != null){
			throw new RuntimeException();
		}
	}

	public static synchronized SingletonDemo06 getInstance(){
		if (instance == null){
			instance = new SingletonDemo06();
		}
		return instance;
	}

	/*private Object readResolve() throws ObjectStreamException{
		return instance;
	}*/

}

然後使用反序列化來構造多個對象:

public class Client2 {

	public static void main(String[] args) throws Exception {

		SingletonDemo06 s1 = SingletonDemo06.getInstance();
		SingletonDemo06 s2 = SingletonDemo06.getInstance();

		System.out.println(s1);
		System.out.println(s2);

		FileOutputStream fos = new FileOutputStream("d:/a.txt");
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(s1);
		oos.close();

		ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/a.txt"));
		SingletonDemo06 s3 = (SingletonDemo06) ois.readObject();
		System.out.println(s3);
	}

}

運行結果如下:

可以看到,s1和s2爲同一對象,而s3爲新的對象。

可以通過定義readResolve()方法來防止獲得不同對象。將SingletonDemo06中的註釋解開,再執行Client2,可以看到,此時的s3和s1s2爲同一對象,沒有被新建。

結論:反序列化時,可以通過定義readResolve()方法來防止獲得不同對象,因爲反序列化時,如果定義了readResolve()則直接返回此方法指定的對象(實際是一種回調),而不需要單獨再創建新對象。

五種單例模式在多線程環境下的效率

public class Client3 {

	public static void main(String[] args) throws Exception {

		long start = System.currentTimeMillis();

		int threadNum = 10;
		final CountDownLatch countDownLatch = new CountDownLatch(threadNum);

		for (int i=0; i<10; i++){
			new Thread(new Runnable() {
				@Override
				public void run() {
					for (int i=0; i<1000000; i++){
						Object o = SingletonDemo01.getInstance();
					}

					countDownLatch.countDown();

				}
			}) .start();
		}

		//main線程阻塞,直到計數器變爲0,纔會繼續往下執行
		countDownLatch.await();

		long end = System.currentTimeMillis();
		System.out.println("總耗時:" + (end-start));
	}

}

這裏用到了一個類CountDownLatch:同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許一個或者多個線程一直等待。

countDown():當前線程調用此方法,則計數減一(建議放在finally裏面執行)。

await():調用此方法會一直阻塞當前線程,直到計數器的值爲0。

執行Client3,依次用SingletonDemo01-06來進行測試,可得出以下結果(由快到慢):餓漢式、靜態內部類式、枚舉式、雙重檢測鎖式、懶漢式。其中,懶漢式的執行效率最低,跟其他四種不在一個數量級(比如餓漢式耗時20s,懶漢式則爲300s)。

 

以上爲單例模式的學習筆記,此文章爲尚學堂視頻的學習筆記+自己總結。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章