1. 單例模式筆記

1. 單例模式

1.1 核心作用

保證一個類只有一個實例,並且提供一個訪問該實例的全局訪問點

1.2 常見應用場景

  • Windows的Task Manager(任務管理器)就是很典型的單例模式
  • Windows的Recycle Bin(回收站)也是典型的單例應用,在整個系統運行過程中,回收站一直維護着僅有的一個實例
  • 項目中,讀取配置文件的類,一般也只有一個對象,沒有必要每次使用配置文件數據時都new一個對象去讀取
  • 網站的計數器一般也採用單例模式實現,否則難以同步
  • 應用程序的日誌應用,一般都採用單例模式實現,這一般是由於共享的日誌一直處於打開狀態,因爲只能有一個實例去操作,否則不好追加
  • 數據庫連接池的設計一般也是採用單例模式,因爲數據庫連接是一種數據庫資源
  • 操作系統的文件系統,也是大的單例模式實現的具體例子,一個操作系統只能有一個文件系統
  • Application也是單例的典型應用(Servlet編程中會涉及到)
  • 在Spring中,每個Bean默認都是單例的
  • 在Servlet編程中,每個Servlet也是單例的
  • 在SpringMVC框架/Struts1框架中,控制器對象也是單例

1.3 優點

  • 只生成一個實例,減少了系統性能開銷:當一個對象的產生需要比較多的資源時,如讀取配置,產生其他依賴對象時,則可以通過在應用啓動時直接產生一個單例對象,然後永久駐留內存的方式解決。
  • 單例模式可以再系統設置全局的訪問點,優化共享資源訪問,例如可以設計一個單例類,負責所有數據表的映射處理。

1.4 常見的五種單例模式實現方法

  • 主要

    • 餓漢式(線程安全,調用效率高,但是,不能延遲加載)
    • 懶漢式(線程安全,調用效率不高,但是,可以延遲加載)
  • 其他

    • 雙重檢測瑣式(由於JVM底層內部模型原因,偶爾會出問題,不建議使用)
    • 靜態內部式(線程安全,調用效率高,但是,可以延遲加載)
    • 枚舉單例(線程安全,調用效率高,但是,不能延遲加載)

如何選用

  1. 單例對象 佔用 資源少,不需要 延遲加載:枚舉式好於餓漢式
  2. 單例對象 佔用 資源大,需要 延遲加載:靜態內部類式好於懶漢式

2. 單例模式的5種實現

2.1 餓漢式(單例對象立即加載)

餓漢式單例模式代碼中,static變量會在類裝載時初始化此時也不會涉及多個線程對象訪問該對象問題。虛擬機保證只會裝載一次該類,肯定不會發生併發訪問的問題。因此,可以省略synchronized關鍵字。

/**
 * 測試餓漢式單例模式
 * @author kevin
 *
 */
public class SingletonDemo1 {
	
	//類初始化時,立即加載這個對象(沒有延時加載的優勢)。加載類時,天然的是線程安全的!
	private static SingletonDemo1 instance = new SingletonDemo1();
	
	private SingletonDemo1() {
		
	}
	
	//方法沒有同步,調用效率高!
	public static SingletonDemo1 getInstance() {
		return instance;
	}

}

測試類:

public class Client1 {

	public static void main(String[] args) {
		SingletonDemo1 s1 = SingletonDemo1.getInstance();
		SingletonDemo1 s2 = SingletonDemo1.getInstance();
		System.out.println(s1 == s2);//true
		System.out.println(s1.hashCode()==s2.hashCode());//true
		
	}

}

問題:如果只是加載本類,而不是要調用getInstance(),甚至永遠沒有調用,則會造成資源浪費

2.2 懶漢式(單例對象延遲加載)

lazy load 延遲加載,懶加載! 真正用的時候才加載!

/**
 * 測試懶漢式單例模式
 * @author kevin
 *
 */
public class SingletonDemo2 {
	
	//類初始化時,不初始化這個對象(延時加載,真正用的時候再創建)。
	private static SingletonDemo2 instance;
	
	private SingletonDemo2(){
		
	}
	
	//方法同步,調用效率低!
	public static synchronized SingletonDemo2 getInstance() {
		if(instance == null) {
			instance = new SingletonDemo2();
		}
		return instance;
	}

}

測試類:

public class Client1 {

	public static void main(String[] args) {
		
		SingletonDemo2 s3 = SingletonDemo2.getInstance();
		SingletonDemo2 s4 = SingletonDemo2.getInstance();
		System.out.println(s3 == s4);//true

	}

}

問題:資源利用率高了,但是,每次調用getInstance()方法都要同步,併發效率較低。

2.3 雙重檢測鎖實現

將同步內容放到if內部,提高了執行的效率,不必每次獲取對象時都進行同步,只有第一次才同步,創建了以後就沒必要了。

/**
 * 雙重檢索實現單例模式
 * @author kevin
 *
 */
public class SingletonDemo3 {
	
	private static SingletonDemo3 instance = null;
	
	private SingletonDemo3() {
		
	}
	
	public static SingletonDemo3 getInstance() {
		if(instance == null) {
			SingletonDemo3 sc;
			synchronized (SingletonDemo3.class) {
				sc = instance;
				if(sc == null) {
					synchronized (SingletonDemo3.class) {
						if(sc == null) {
							sc = new SingletonDemo3();
						}
					}
					instance = sc;
				}
			}
		}
		return instance;
	}

}

測試類:

public class Client1 {

	public static void main(String[] args) {
		
		SingletonDemo3 s5 = SingletonDemo3.getInstance();
		SingletonDemo3 s6 = SingletonDemo3.getInstance();
		System.out.println(s5 == s6);//true
		

	}

}

問題:由於編譯器優化原因和JVM底層內部模型原因,偶爾會出問題,不建議使用

2.4 靜態內部類實現方式(單例對象延遲加載)

/**
 * 測試靜態內部類實現單例模式
 * @author kevin
 *
 */
public class SingletonDemo4 {
	
	private static class SingletonClassInstance {
		private static final SingletonDemo4 instance = new SingletonDemo4();
	}
	
	private SingletonDemo4() {
		
	}
	
	//方法沒有同步,調用效率高
	public static SingletonDemo4 getInstance() {
		return SingletonClassInstance.instance;
	}

}

測試類:

public class Client1 {

	public static void main(String[] args) {
		
		SingletonDemo4 s7 = SingletonDemo4.getInstance();
		SingletonDemo4 s8 = SingletonDemo4.getInstance();
		System.out.println(s7 == s8);//true
		

	}

}

要點:

  • 外部類沒有static屬性,則不會像餓漢式那樣立即加載對象。
  • 只有真正調用getInstance()纔會加載靜態內部類,加載類時線程是安全的,instance是static final類型,保證了內存中只有一個這樣實例存在,而且只能被賦值一次,從而保證了線程安全性。
  • 兼備了併發高效調用和延遲加載的優勢!

2.5 枚舉實現方式

/**
 * 測試枚舉實現單例模式
 * @author kevin
 *
 */
public enum SingletonDemo5 {
	
	//這個枚舉元素 ,代表了Singleton的一個實例
	INSTANCE;
	
	//添加自己需要的操作
	public void singletonOperation() {
		
	}

}

測試類:

public class Client1 {

	public static void main(String[] args) {
		
		SingletonDemo5 s9 = SingletonDemo5.INSTANCE;
		SingletonDemo5 s10 = SingletonDemo5.INSTANCE;
		System.out.println(s9 == s10);//true
		

	}

}

優點:

  • 實現簡單
  • 枚舉本身就是單例模式。由jvm從根本上提供保障!避免通過反射和反序列化的漏洞。

缺點:

  • 無延遲加載

3. 反射和反序列化破解單例模式

1) 反射可以破解上面幾種單例模式(非枚舉式)的實現方式。

可以在構造方法中手動拋出異常控制

2) 反序列化可以破解上面幾種單例模式(非枚舉式)實現方式。

可以通過定義readResolve()防止獲得不同對象

3.1 反射破解單例模式

定義個懶漢式實現的單例模式:

public class SingletonDemo6 {
	
	//類初始化時,不初始化這個對象(延時加載,真正用的時候再創建)。
	private static SingletonDemo6 instance;
	
	private SingletonDemo6(){
		
	}
	
	//方法同步,調用效率低!
	public static synchronized SingletonDemo6 getInstance() {
		if(instance == null) {
			instance = new SingletonDemo6();
		}
		return instance;
	}

}

使用反射破解單例模式測試類:Client2.java:

public class Client2 {

	public static void main(String[] args) throws Exception {
		SingletonDemo6 s1 = SingletonDemo6.getInstance();
		SingletonDemo6 s2 = SingletonDemo6.getInstance();
		System.out.println(s1);
		System.out.println(s2);
		
		//通過反射的方式直接調用私有的構造器
		Class clazz = Class.forName("com.stormkai.singleton.demo.SingletonDemo6");
		
		Constructor<SingletonDemo6> c = clazz.getDeclaredConstructor(null);
		c.setAccessible(true);
		
		SingletonDemo6 s3 = c.newInstance();
		SingletonDemo6 s4 = c.newInstance();
		
		System.out.println(s3);
		System.out.println(s4);
		
	}

}

測試結果爲:

com.stormkai.singleton.demo.SingletonDemo6@15db9742
com.stormkai.singleton.demo.SingletonDemo6@15db9742
com.stormkai.singleton.demo.SingletonDemo6@6d06d69c
com.stormkai.singleton.demo.SingletonDemo6@7852e922

3.1.1 防止反射破解單例模式

只需要在單例的私有構造器中添加如下代碼:

if(instance != null) {
    throw new RuntimeException();
}

則反射破解單例模式時候就會報java.lang.reflect.InvocationTargetException.

代碼如下:

public class SingletonDemo6 {
	
	//類初始化時,不初始化這個對象(延時加載,真正用的時候再創建)。
	private static SingletonDemo6 instance;
	
	private SingletonDemo6(){
		
		//添加如下代碼,反射破解單例時候會報java.lang.reflect.InvocationTargetException
		if(instance != null) {
			throw new RuntimeException();
		}
		
	}
	
	//方法同步,調用效率低!
	public static synchronized SingletonDemo6 getInstance() {
		if(instance == null) {
			instance = new SingletonDemo6();
		}
		return instance;
	}

}

再次執行Client2.java,測試結果爲:

com.stormkai.singleton.demo.SingletonDemo6@15db9742
com.stormkai.singleton.demo.SingletonDemo6@15db9742
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
	at java.lang.reflect.Constructor.newInstance(Unknown Source)
	at com.stormkai.singleton.demo.Client2.main(Client2.java:30)
Caused by: java.lang.RuntimeException
	at com.stormkai.singleton.demo.SingletonDemo6.<init>(SingletonDemo6.java:20)
	... 5 more

3.2 反序列化破解單例模式

給單例的實現類SingletonDemo6.java添加序列化implements Serializable

public class SingletonDemo6 implements Serializable {
	
	//類初始化時,不初始化這個對象(延時加載,真正用的時候再創建)。
	private static SingletonDemo6 instance;
	
	private SingletonDemo6(){
		
		//添加如下代碼,反射破解單例時候會報java.lang.reflect.InvocationTargetException
		if(instance != null) {
			throw new RuntimeException();
		}
		
	}
	
	//方法同步,調用效率低!
	public static synchronized SingletonDemo6 getInstance() {
		if(instance == null) {
			instance = new SingletonDemo6();
		}
		return instance;
	}

}

使用反序列化破解單例模式測試類:Client2.java:

public class Client2 {

	public static void main(String[] args) throws Exception {
		SingletonDemo6 s1 = SingletonDemo6.getInstance();
		SingletonDemo6 s2 = SingletonDemo6.getInstance();
		System.out.println(s1);
		System.out.println(s2);
		
		
		//通過反序列化的方式構造多個對象
		FileOutputStream fos = new FileOutputStream("d:/aaa.txt");
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(s1);
		oos.close();
		fos.close();
		
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/aaa.txt"));
		SingletonDemo6 s3 = (SingletonDemo6)ois.readObject();
		System.out.println(s3);
		
	}

}

測試結果爲:

com.stormkai.singleton.demo.SingletonDemo6@15db9742
com.stormkai.singleton.demo.SingletonDemo6@15db9742
com.stormkai.singleton.demo.SingletonDemo6@776ec8df

3.2.1 防止反序列化破解單例模式

反序列化時,如果定義了readResolve()則直接返回此方法指定的對象。而不需要單獨再創建新對象!

修改單例實現類SingletonDemo6.java

public class SingletonDemo6 implements Serializable {
	
	//類初始化時,不初始化這個對象(延時加載,真正用的時候再創建)。
	private static SingletonDemo6 instance;
	
	private SingletonDemo6(){
		
		//添加如下代碼,反射破解單例時候會報java.lang.reflect.InvocationTargetException
		if(instance != null) {
			throw new RuntimeException();
		}
		
	}
	
	//方法同步,調用效率低!
	public static synchronized SingletonDemo6 getInstance() {
		if(instance == null) {
			instance = new SingletonDemo6();
		}
		return instance;
	}
	
	//反序列化時,如果定義了readResolve()則直接返回此方法指定的對象。而不需要單獨再創建新對象!
	private Object readResolve() throws ObjectStreamException {
		return instance;
	}

}

再次執行Client2.java,測試結果爲:

com.stormkai.singleton.demo.SingletonDemo6@15db9742
com.stormkai.singleton.demo.SingletonDemo6@15db9742
com.stormkai.singleton.demo.SingletonDemo6@15db9742

4. 常見的五種模式在多線程環境下的效率測試

測試類:

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 < threadNum; i++) {
			new Thread(new Runnable() {
				
				@Override
				public void run() {
					for(int i=0;i<1000000;i++){
						Object o = SingletonDemo2.getInstance();
						//Object o = SingletonDemo5.INSTANCE;
					}
					
					countDownLatch.countDown();
					
				}
			}).start();
		}
		
		countDownLatch.await();	//main線程阻塞,直到計數器變爲0,纔會繼續往下執行!
		
		long end = System.currentTimeMillis();
		System.out.println("總耗時:"+(end-start));

	}

}

CountDownLatch

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

  • countDown():當前線程調此方法,則計數減一(建議放在finally裏執行)
  • await():調用此方法會一直阻塞當前線程,直到計時器值爲0
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章